diff --git a/apps/docs/content/guides/zerops-yaml-advanced.mdx b/apps/docs/content/guides/zerops-yaml-advanced.mdx index acf97e9d..2221bc0b 100644 --- a/apps/docs/content/guides/zerops-yaml-advanced.mdx +++ b/apps/docs/content/guides/zerops-yaml-advanced.mdx @@ -92,12 +92,12 @@ run: values: { X-Frame-Options: "'DENY'" } ``` -- **`cors`**: Sets Access-Control-Allow-Origin. `"*"` auto-converted to `'*'` -- **`redirects[]`**: `from` (wildcards `*`), `to`, `status`, `preservePath`, `preserveQuery` -- **`headers[]`**: `for` (path pattern), `values` (header key-value pairs) -- **`root`**: Custom root directory +- **`cors`**: Sets Access-Control-Allow-Origin, -Allow-Methods, -Allow-Headers and -Expose-Headers to the same value. `"*"` auto-converted to `'*'` +- **`redirects[]`**: `from` (exact path, or prefix with trailing `*`, or absolute URL matched on the domain), `to`, `status` (omit for a masked redirect, `301`/`302` only for absolute `from`), `preservePath`, `preserveQuery` (both only with a `status`) +- **`headers[]`**: `for` (same matching as `from`, `"/*"` for everything, `"/"` is the homepage only), `values` (header key-value pairs, quotes included: `"'DENY'"`). Only the best-matching rule applies, rules are not merged. +- **`root`**: Folder served, relative to `/var/www`. The Static service ignores `run.documentRoot`, this is its replacement. -**DO NOT** use on non-static services -- silently ignored. +**DO NOT** use on non-static services -- silently ignored. Conversely `documentRoot` is ignored on Static, and `siteConfigPath` is ignored on Static whenever `routing` is present. --- @@ -135,7 +135,7 @@ Available runtimes and versions are listed in **Service Stacks (live)** -- injec - The OS is part of the base value — `ubuntu/nodejs@22` or `alpine/nodejs@22` — with no separate `os:` field; use the same prefix in `build.base` and `run.base` (a bare `nodejs@22` resolves to Alpine, and `run.base` rewrites the service OS on deploy) - PHP: build `/php@X`, run `/php-nginx@X` or `/php-apache@X` (different bases) - Deno: ships only as `ubuntu/deno@X` (no Alpine image). Gleam runs on both Alpine and Ubuntu. -- Static sites: build `alpine/nodejs@latest`, run `static` +- Static sites: build `alpine/nodejs@latest`, run `alpine/static` (the bare `static` shorthand means the same) - `@latest` = newest stable version --- diff --git a/apps/docs/content/nginx/faq.mdx b/apps/docs/content/nginx/faq.mdx index eafe0dd2..8ec3cc49 100644 --- a/apps/docs/content/nginx/faq.mdx +++ b/apps/docs/content/nginx/faq.mdx @@ -6,7 +6,13 @@ description: Get quick answers to your related questions about Nginx from freque import { FAQ, FAQItem } from '/src/components/Faq'; + + Both serve static files with Nginx. The [Static service](/static/overview) generates the Nginx configuration from a declarative `run.routing` section (redirects, headers, CORS) and has Prerender.io built in. The Nginx service gives you the default configuration or your own file via `run.siteConfigPath`, which is the way to go for `proxy_pass`, caching or extra ports. See the [comparison](/static/overview#static-or-nginx-service). + + + `run.routing` is only read by the Static service. The Nginx service ignores it without an error. Either switch `run.base` to `alpine/static`, or express the redirects and headers in a custom configuration set via [`siteConfigPath`](/nginx/how-to/customize-web-server). + - Zerops provides built-in prerender.io support. Simply set the `PRERENDER_TOKEN` environment variable with your prerender.io service token. See our [prerender.io documentation](/nginx/how-to/env-variables#prerenderio-support) for details. + Prerender.io support is built into the [Static service](/static/overview#seo-with-prerender), not into the Nginx service. On the Nginx service you can add the prerender rules to your own configuration set via [`siteConfigPath`](/nginx/how-to/customize-web-server), or switch to the Static service. diff --git a/apps/docs/content/nginx/how-to/build-pipeline.mdx b/apps/docs/content/nginx/how-to/build-pipeline.mdx index 8c683976..48d71e43 100644 --- a/apps/docs/content/nginx/how-to/build-pipeline.mdx +++ b/apps/docs/content/nginx/how-to/build-pipeline.mdx @@ -8,6 +8,10 @@ import UnorderedCodeList from 'docs/src/components/UnorderedCodeList'; Zerops provides a customizable build and runtime environment for your static content. +:::note Nginx service vs Static service +This page covers the Nginx service (`run.base: alpine/nginx@latest` or `ubuntu/nginx@latest`). The [Static service](/static/overview) (`run.base: alpine/static`) uses the same build and deploy pipeline but is configured through `run.routing` instead of `documentRoot` and `siteConfigPath`. The `run.routing` section is ignored by the Nginx service. See [Static or Nginx?](/static/overview#static-or-nginx-service). +::: + :::tip Two Deployment Approaches You can use the Nginx service in two ways: - **Runtime only**: Deploy pre-built static files directly (no build phase needed) @@ -35,9 +39,6 @@ zerops: # REQUIRED (if using build). Set the base technology for the build environment: base: nodejs@latest - # OPTIONAL. Set the operating system for the build environment. - # os: ubuntu - # OPTIONAL. Customize the build environment by installing additional packages # or tools to the base build environment. # prepareCommands: @@ -62,8 +63,8 @@ zerops: # ==== how to run your application ==== run: - # OPTIONAL. Sets the base technology for the runtime environment: - base: nginx@latest + # OPTIONAL. Sets the base technology and OS for the runtime environment: + base: alpine/nginx@latest # OPTIONAL. Customize the runtime Nginx environment by installing additional # dependencies to the base Nginx runtime environment. @@ -139,14 +140,14 @@ zerops: # ==== how to run your application ==== run: - # OPTIONAL. Sets the base technology for the runtime environment: - base: nginx@latest + # OPTIONAL. Sets the base technology and OS for the runtime environment: + base: alpine/nginx@latest ... ```

The base runtime environment contains {data.alpine.default}, the - selected major version of Nginx, Zerops command line tool and `composer`, `git` and `wget`. + selected major version of Nginx, Zerops command line tool, `git`, `wget` and `curl`.

:::info @@ -169,7 +170,7 @@ zerops: run: # OPTIONAL. Sets the base technology for the runtime environment: base: - - nginx@latest + - alpine/nginx@latest prepareCommands: - zsc add go@latest ... @@ -181,14 +182,7 @@ To customize your build environment use the `prepareCommands` attribute. ### os -_OPTIONAL._ Sets the operating system for the runtime environment. - -Following options are available: - -- `alpine` -- `ubuntu` - -Default value is `alpine`. +_DEPRECATED._ The operating system is part of the `base` value: `alpine/nginx@latest` or `ubuntu/nginx@latest`. Do not set `os` separately. We are currently using following os version: @@ -250,7 +244,7 @@ _OPTIONAL._ Customizes the Nginx runtime environment by installing additional de

The base Nginx environment contains {data.alpine.default}, the selected - major version of Nginx, Zerops command line tool and `composer`, `git` and `wget`. To install + major version of Nginx, Zerops command line tool, `git`, `wget` and `curl`. To install additional packages or tools add one or more prepare commands:

@@ -314,7 +308,7 @@ You can configure your prepare commands to be run in a single shell instance or

The prepare runtime container contains {data.alpine.default}, the - selected major version of Nginx, Zerops command line tool and `composer`, `git` and `wget`. + selected major version of Nginx, Zerops command line tool, `git`, `wget` and `curl`.

The prepare runtime container does not contain your application code nor the built application. If you need to copy some folders or files from the build container to the runtime container (e.g. a configuration file) use the `addToRunPrepare` attribute in the build section of your chosen technology. @@ -412,12 +406,30 @@ zerops: _OPTIONAL._ Sets the custom Nginx configuration. :::info -If you don't set your custom configuratiin Zerops applies the [default](/nginx/how-to/customize-web-server#default-nginx-configuration) configuration. +If you don't set your custom configuration Zerops applies the [default](/nginx/how-to/customize-web-server#default-nginx-configuration) configuration. ::: -The file must be deployed in the runtime container. Enter the path to the file relative to the `/var/www` folder. +The file must be deployed in the runtime container. Enter the path to the file relative to the `/var/www` folder. A file with the `.tmpl` extension is rendered as a template (`{{.DocumentRoot}}`, `{{.Environment.NAME}}`), any other file is used verbatim. Read more about the [web server customization](/nginx/how-to/customize-web-server). +```yaml +zerops: + # hostname of your service + - setup: app + # ==== how to build your application ==== + build: + ... + deployFiles: + - dist + - site_config.tmpl + + # ==== how to run your application ==== + run: + documentRoot: dist + # OPTIONAL. Path to your Nginx configuration, relative to /var/www. + siteConfigPath: site_config.tmpl +``` + ### envVariables _OPTIONAL._ Defines the environment variables for the runtime environment. diff --git a/apps/docs/content/nginx/how-to/create.mdx b/apps/docs/content/nginx/how-to/create.mdx index a6aa922e..57ad7627 100644 --- a/apps/docs/content/nginx/how-to/create.mdx +++ b/apps/docs/content/nginx/how-to/create.mdx @@ -11,6 +11,10 @@ import ResourceTable from '/src/components/ResourceTable'; The Nginx static service contains the Nginx web server optimized for your static content. Nginx static service is highly scalable and customisable to suit both development and production. +:::tip +If you only need redirects, headers and CORS on top of your static files, the [Static service](/static/overview) configures them declaratively without any Nginx configuration. See [Static or Nginx?](/static/overview#static-or-nginx-service). +::: + ## Create Nginx static service using Zerops GUI First, set up a project in Zerops GUI. Then go to the project dashboard page and choose **Add new service** in the left menu in the **Services** block. Then add a new Nginx static service: @@ -84,11 +88,11 @@ project: services: - # service name hostname: app - # service type and version number in nginx@{version} format - type: nginx@latest + # service type, OS and version number in {os}/nginx@{version} format + type: ubuntu/nginx@latest # defines the minimum number of containers for horizontal autoscaling minContainers: 1 - # defines the maximum number of containers for horizontal autoscaling. Max value = 6. + # defines the maximum number of containers for horizontal autoscaling. maxContainers: 6 # optional: create env variables envSecrets: @@ -96,7 +100,7 @@ services: S3_ACCESS_SECRET: 'ogFthuiLYki8XoL73opSCQ' ``` -The yaml file describes your future project infrastructure. The project will contain one Nginx version 8.1 service with default [auto scaling](/nginx/how-to/scaling) configuration. Hostname will be set to "app", the internal port(s) the service listens on will be defined later in the [zerops.yaml](/nginx/how-to/build-pipeline#ports). Following secret env variables will be configured: +The yaml file describes your future project infrastructure. The project will contain one Nginx version 1.22 service with default [auto scaling](/nginx/how-to/scaling) configuration. Hostname will be set to "app", the internal port(s) the service listens on will be defined later in the [zerops.yaml](/nginx/how-to/build-pipeline#ports). Following secret env variables will be configured: ```env S3_ACCESS_KEY_ID="P8cX1vVVb" @@ -122,8 +126,8 @@ project: services: - # service name hostname: app - # service type and version number in nginx@{version} format - type: nginx@latest + # service type, OS and version number in {os}/nginx@{version} format + type: ubuntu/nginx@latest # optional: vertical auto scaling customization verticalAutoscaling: cpuMode: DEDICATED @@ -136,9 +140,9 @@ services: startCpuCoreCount: 3 minFreeRamGB: 0.5 minFreeRamPercent: 20 - # defines the minimum number of containers for horizontal autoscaling. Max value = 6. + # defines the minimum number of containers for horizontal autoscaling. minContainers: 2 - # defines the maximum number of containers for horizontal autoscaling. Max value = 6. + # defines the maximum number of containers for horizontal autoscaling. maxContainers: 4 # optional: create secret env variables envSecrets: @@ -344,11 +348,11 @@ project: services: - # service name hostname: app - # service type and version number in nginx@{version} format - type: nginx@latest + # service type, OS and version number in {os}/nginx@{version} format + type: ubuntu/nginx@latest # defines the minimum number of containers for horizontal autoscaling minContainers: 1 - # defines the maximum number of containers for horizontal autoscaling. Max value = 6. + # defines the maximum number of containers for horizontal autoscaling. maxContainers: 6 # optional: create env variables envSecrets: diff --git a/apps/docs/content/nginx/how-to/customize-runtime.mdx b/apps/docs/content/nginx/how-to/customize-runtime.mdx index 51fad9b2..0bc616bc 100644 --- a/apps/docs/content/nginx/how-to/customize-runtime.mdx +++ b/apps/docs/content/nginx/how-to/customize-runtime.mdx @@ -20,10 +20,10 @@ You should not include your application code in the custom runtime image, as you The default Nginx Static runtime environment contains: -- {data.alpine.default} +- {data.alpine.default} with `alpine/nginx@latest`, or Ubuntu with `ubuntu/nginx@latest` - Selected version of Nginx when the runtime service was created - [zCLI](/references/cli) -- Git +- Git, wget and curl ### When You Need a Custom Runtime Image @@ -41,6 +41,8 @@ Here are Nginx Static-specific examples of configuring custom runtime images in +Use `sudo apk add` on Alpine and `sudo apt-get install -y` on Ubuntu. + For complete configuration details, see the [runtime prepare phase configuration guide](/features/pipeline#configuration). ## Process and Caching diff --git a/apps/docs/content/nginx/how-to/customize-web-server.mdx b/apps/docs/content/nginx/how-to/customize-web-server.mdx index a61bf604..2a9f3c9f 100644 --- a/apps/docs/content/nginx/how-to/customize-web-server.mdx +++ b/apps/docs/content/nginx/how-to/customize-web-server.mdx @@ -15,6 +15,11 @@ server { server_name _; root {{.DocumentRoot}}; + location ^~ /.git { + deny all; + return 404; + } + location / { try_files $uri $uri/ /index.html; } @@ -24,15 +29,23 @@ server { } ``` -The configuration contains 2 variables: +The configuration contains one variable: - **`{{.DocumentRoot}}`** is replaced by the `run.documentRoot` attribute from the `zerops.yaml`. If the attribute is not specified, the default value `/var/www` is used. +With this configuration a request is served from the exact path, then from `index.html` inside a directory (a request for `/docs` is redirected to `/docs/` first), and falls back to `/index.html` for everything else, which makes client-side routing of Single Page Applications work. Extensionless URLs such as `/about` for `about.html` are **not** resolved, and anything under `/.git` returns 404. Requests for a nonexistent path return `/index.html` with status 200, not 404. + +The surrounding `nginx.conf` enables gzip for text-based content types, hides the Nginx version and allows request bodies up to 10 GB. + +:::note +Redirects, custom headers and CORS have no declarative configuration on the Nginx service. Write them in your own configuration as described below. The `run.routing` section of `zerops.yaml` belongs to the [Static service](/static/overview#routing--configuration) and is ignored here. +::: + ## Customize Nginx configuration Follow these steps to customize the Nginx configuration in Nginx static service: -1. Create a **.tmpl** file with the Nginx configuration in your repository. +1. Create a **.tmpl** file with the Nginx configuration in your repository. The file replaces the whole default `server` block shown above, so start from a copy of it. 2. Optionally use following variables: @@ -46,8 +59,16 @@ root {{.DocumentRoot}}; - **`{{.Environment.ENV_NAME}}`** is replaced by the [env variable](/nginx/how-to/env-variables) value. The env variable must be either defined in [run.envVariables](/nginx/how-to/build-pipeline#envvariables) in `zerops.yaml` or set as a [secret](/nginx/how-to/env-variables#set-secret-env-variables-in-zerops-gui) or [generated](/nginx/how-to/env-variables#generated-env-variables) env variable in Zerops GUI. +Example: + +``` +location /api/ { + proxy_pass http://{{.Environment.API_HOSTNAME}}:3000/; +} +``` + :::caution -Use the **.tmpl** file extension to make Zerops interpret the file as a template. Zerops will replace the supported variables listed above. +Use the **.tmpl** file extension to make Zerops interpret the file as a template. Zerops will replace the supported variables listed above. A file with any other extension is copied to Nginx verbatim, without variable replacement. ::: 3. Check that your Nginx configuration is consistent with Zerops requirements: @@ -55,6 +76,7 @@ Use the **.tmpl** file extension to make Zerops interpret the file as a template - Do not use IP addresses in the `listen` directive - If you use other ports than `:80` in the `listen` directive, add them to the `run.ports` in your `zerops.yaml` as well. - Do not use the port **:443**. All the incoming `https://` traffic is terminated on the Zerops internal balancer where the SSL certificate is installed and the request is forwarded to your Nginx static service as a **http://** on the port **:80**. +- Keep the `access_log` and `error_log` directives from the default configuration so that the logs show up in Zerops. 4. Add the `siteConfigPath` to the run section of your `zerops.yaml` @@ -67,37 +89,72 @@ zerops: # REQUIRED. Set the base technology for the build environment: base: nodejs@latest + buildCommands: + - npm i + - npm run build + # REQUIRED. Select which files / folders to deploy after # the build has successfully finished deployFiles: - - vendor - - public + - dist + - site_config.tmpl # ==== how to run your application ==== run: - documentRoot: public + base: alpine/nginx@latest - # OPTIONAL. Sets the custom Nginx or Apache configuration. The file must be deployed in the runtime container. Enter the path to the file relative to the /var/www folder + # OPTIONAL. Folder served by Nginx, relative to /var/www + documentRoot: dist + + # OPTIONAL. Sets the custom Nginx configuration. The file must be deployed in the runtime container. Enter the path to the file relative to the /var/www folder siteConfigPath: site_config.tmpl ``` 5. Ensure that the `build.deployFiles` contains the folder with the `siteConfigPath` or add the path to the Nginx config file to the `deployFiles` list. Zerops will deploy the file to the runtime container(s). -6. [Trigger](/nginx/how-to/trigger-pipeline) the build & deploy pipeline. +:::tip +Everything under the document root is publicly served, including a configuration file deployed there. Keep the configuration outside the `documentRoot` folder (as in the example above, where `documentRoot` is `dist` and the template sits next to it in `/var/www`). +::: -## SEO & Prerender Support +6. [Trigger](/nginx/how-to/trigger-pipeline) the build & deploy pipeline. The configuration is applied every time a container starts, and the deploy fails if the file is missing. -Single Page Applications and JavaScript-heavy sites render content client-side, which most crawlers can't process—they see an empty page instead of your content. This affects traditional search engines, social media platforms, and AI tools like ChatGPT, Perplexity, and Claude. +## Common customizations -### Built-in Prerender.io Integration +Serve extensionless `.html` pages and keep the SPA fallback: -The default Nginx configuration includes automatic [Prerender.io](https://prerender.io) support. When enabled, it detects crawler requests (including AI crawlers) and serves them pre-rendered HTML while your users get the full interactive experience. +``` +location / { + try_files $uri $uri.html $uri/index.html /index.html =404; +} +``` + +Redirect an old path permanently: + +``` +location = /old-page { + return 301 /new-page; +} +``` + +Add headers to every response: -### Setup +``` +add_header X-Frame-Options 'DENY' always; +add_header X-Content-Type-Options 'nosniff' always; +``` + +Proxy a path to another service of the project (the hostname is the other service's name): + +``` +location /api/ { + proxy_pass http://api:3000/; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +} +``` -To enable prerender support: +## SEO & Prerender Support -1. Set the `PRERENDER_TOKEN` environment variable with your Prerender.io token (see [environment variables](/nginx/how-to/env-variables#prerenderio-support)) -2. Optionally set `PRERENDER_HOST` if using a custom prerender server +Prerender.io support is not part of the default Nginx service configuration. It is built into the [Static service](/static/overview#seo-with-prerender), which enables it as soon as `PRERENDER_TOKEN` is set. -The Nginx configuration will automatically handle the rest—no additional configuration needed. \ No newline at end of file +If you need prerendering together with a hand-written Nginx configuration, either start from the configuration generated by a Static service (open the Static service in the GUI and choose **Need to switch to full Nginx service?**, or copy `/etc/nginx/sites-enabled/default.site` from one of its containers) or add the [Prerender.io Nginx snippet](https://docs.prerender.io/docs/nginx-2) to your own template. diff --git a/apps/docs/content/nginx/how-to/env-variables.mdx b/apps/docs/content/nginx/how-to/env-variables.mdx index f71798e0..5ea4b23a 100644 --- a/apps/docs/content/nginx/how-to/env-variables.mdx +++ b/apps/docs/content/nginx/how-to/env-variables.mdx @@ -11,44 +11,10 @@ import EnvVariablesTemplate from '/src/components/content/env-variables.mdx'; -## Prerender.io Support +## Environment variables in the Nginx configuration -Zerops provides built-in prerender.io support for SEO optimization. Configure it using these environment variables: +Nginx itself does not read environment variables. To use them in your configuration, deploy a `.tmpl` file via [`siteConfigPath`](/nginx/how-to/customize-web-server#customize-nginx-configuration) and reference variables as `{{.Environment.NAME}}`. Zerops renders the template every time a container starts. - - - - - - - - - - - - - - - - - - - - - - - -
VariableRequiredDescriptionDefault
PRERENDER_TOKENYesYour prerender.io service token-
PRERENDER_HOSTNoPrerender service hostservice.prerender.io
- -:::tip -Set `PRERENDER_TOKEN` as a secret environment variable in Zerops GUI for security. -::: - -Example in zerops.yaml: -```yaml -zerops: - - setup: app - run: - envVariables: - PRERENDER_HOST: "custom.prerender.host" -``` \ No newline at end of file +:::note +Prerender.io support (`PRERENDER_TOKEN`, `PRERENDER_HOST`) is a feature of the [Static service](/static/overview#seo-with-prerender). The Nginx service does not act on these variables. +::: \ No newline at end of file diff --git a/apps/docs/content/nginx/overview.mdx b/apps/docs/content/nginx/overview.mdx index f0c4fed7..383171f3 100644 --- a/apps/docs/content/nginx/overview.mdx +++ b/apps/docs/content/nginx/overview.mdx @@ -8,7 +8,11 @@ import Icons from '@theme/Icon'; import LargeCardList from '@site/src/components/LargeCardList'; import LargeCard from '@site/src/components/LargeCard'; -The Nginx static service contains the [Nginx ↗](https://nginx.org/) web server optimized for your static content. +The Nginx static service contains the [Nginx ↗](https://nginx.org/) web server optimized for your static content. You get a plain Nginx with a sensible default configuration and full control over it through your own configuration file. + +:::tip Static or Nginx? +Zerops also offers the [Static service](/static/overview), which generates the Nginx configuration for you from a declarative `run.routing` section (redirects, headers, CORS) and has Prerender.io built in. Choose the Nginx service when you need to write Nginx configuration yourself, for example for `proxy_pass`, caching, or additional ports. The `run.routing` section is ignored by the Nginx service, and `run.documentRoot` is ignored by the Static service. See the [comparison](/static/overview#static-or-nginx-service). +::: ## How to start @@ -65,11 +69,11 @@ The Nginx static service contains the [Nginx ↗](https://nginx.org/) web server }, { type: 'link', - href: '/nginx/how-to/env-variables#prerenderio-support', - label: 'SEO Optimization', + href: '/nginx/how-to/customize-web-server', + label: 'Custom Nginx configuration', customProps: { - icon: Icons['magnifying-glass'], - description: 'Built-in prerender.io support for better SEO.', + icon: Icons['puzzle'], + description: 'Bring your own Nginx configuration with siteConfigPath.', }, }, ]} diff --git a/apps/docs/content/static/overview.mdx b/apps/docs/content/static/overview.mdx index 43eccf0a..cb560c8a 100644 --- a/apps/docs/content/static/overview.mdx +++ b/apps/docs/content/static/overview.mdx @@ -6,7 +6,7 @@ desc: A comprehensive guide to Zerops' Static service, covering configuration, r import DeployButton from '@site/src/components/DeployButton'; import CustomCard from '@site/src/components/CustomCard'; -The Static service provides a way to serve static content through a pre-configured Nginx setup. It balances simplicity with the flexibility needed for modern web applications. +The Static service serves static files (HTML, CSS, JavaScript, images, the build output of any frontend framework) through a pre-configured Nginx. You describe redirects, headers and CORS declaratively in `zerops.yaml` and Zerops generates the Nginx configuration for you. Deploy an Analog app with static hosting in seconds. All you need is a Zerops account. @@ -14,49 +14,125 @@ The Static service provides a way to serve static content through a pre-configur +## Static or Nginx service? + +Zerops offers two services for static content. Both run Nginx, but they are configured differently and support different `zerops.yaml` attributes: + +| | Static service | [Nginx service](/nginx/overview) | +|---|---|---| +| `run.base` | `alpine/static` or `ubuntu/static` | `alpine/nginx@latest` or `ubuntu/nginx@latest` | +| Nginx configuration | Generated from `run.routing` | Built-in default, or your own file via `run.siteConfigPath` | +| Redirects, custom headers, CORS | Declarative `run.routing` | Written by you in the Nginx configuration | +| Fallback for missing paths | `/index.html` (SPA friendly), plus extensionless `.html` pages | `/index.html` | +| Prerender.io for crawlers | Built in, enabled by `PRERENDER_TOKEN` | Not built in | +| Reverse proxy (`proxy_pass`), caching, rate limiting, other ports | Not available | Anything Nginx can do, in your own configuration | +| Document root | `run.routing.root` | `run.documentRoot` | + +Choose **Static** when you deploy framework build output or plain files and need at most redirects, headers and CORS. Choose **Nginx** when you need Nginx directives that `routing` cannot express, typically proxying to another service, response caching, or listening on additional ports. + +:::caution Attributes are not interchangeable +- `run.routing` is read only by the Static service. On any other service, including Nginx, it is silently ignored. +- `run.documentRoot` is ignored by the Static service. Use `run.routing.root` instead. +::: + +:::tip Sending paths to another service +You do not need an Nginx service just to send `/api` to a backend. [Domain access routing](/references/networking/public-access#http-routing-setup) on the project's L7 balancer maps public paths to services and ports, so a Static frontend and an API can share one domain. +::: + ## Quick Start -Add a Static service to your project by including this in your `zerops.yaml`: +Build your frontend with any runtime and hand the output to the Static service: ```yaml title="zerops.yaml" zerops: - setup: app + build: + base: nodejs@latest + buildCommands: + - npm i + - npm run build + deployFiles: + - dist/~ # deploy the *contents* of dist to /var/www run: - os: alpine - base: static + base: alpine/static ``` -## Routing & Configuration +The `~` in `dist/~` deploys the contents of `dist` directly into `/var/www`, so `index.html` ends up at the document root. If you deploy the folder as a whole (`deployFiles: - dist`), point the service at it with [`routing.root: dist`](#document-root). + +If your files are already built, skip the `build` section and [deploy them with zCLI](/references/cli). + +The base carries the operating system: `alpine/static` or `ubuntu/static`. The bare shorthand `static` is accepted and means `alpine/static`. -### Default Behavior +## How requests are served -Every Static service in Zerops comes with built-in defaults optimized for modern web applications, including Single Page Applications. By default, for any incoming request, the service will: -1. Try to serve the exact path (`$uri`) -2. Try with .html extension (`$uri.html`) -3. Look for an index.html in the directory (`$uri/index.html`) -4. Fall back to `/index.html` (suitable for SPAs) -5. Return 404 if none of the above exist +The generated configuration serves files from the [document root](#document-root) and resolves every request in this order: + +1. The exact path (`$uri`) +2. The path with `.html` appended (`$uri.html`), so `/about` serves `about.html` +3. `index.html` inside the directory (`$uri/index.html`), so `/docs` serves `docs/index.html` +4. `/index.html`, which makes client-side routing of Single Page Applications work +5. HTTP 404 if `/index.html` does not exist either + +Other built-in behavior: + +- The service listens on port 80 only. HTTPS is terminated on the Zerops balancer and forwarded as plain HTTP. +- Everything under `/.git` returns 404. +- Gzip compression is enabled for text-based content types. +- Prerender.io is wired in and becomes active once `PRERENDER_TOKEN` is set, see [SEO with Prerender](#seo-with-prerender). :::important SPAs -Single Page Applications work out of the box without any additional configuration. The built-in fallback to `/index.html` ensures that client-side routing functions properly. +Single Page Applications work out of the box. No redirects are needed for client-side routing. A consequence of the fallback is that a request for a nonexistent path returns `/index.html` with status 200, not 404. ::: -### Custom Routing Configuration +## Document root + +By default files are served from `/var/www`, the folder your `deployFiles` land in. To serve a subfolder, set `run.routing.root`. The path is relative to `/var/www`: + +```yaml title="zerops.yaml" +run: + base: alpine/static + routing: + root: dist # serves /var/www/dist +``` + +:::caution +`run.documentRoot` has no effect on the Static service. It is only used by the [Nginx](/nginx/how-to/build-pipeline#documentroot) and PHP services (and inside a [custom `.tmpl` configuration](#custom-nginx-configuration)). +::: -The Static service allows you to configure additional URL routing and redirects through simple YAML configuration, abstracting away the complexity of Nginx configuration. +## Routing & Configuration -Configure custom routing beyond the default behavior in the `run.routing` section of your `zerops.yaml`: +Configure redirects, headers and CORS in the `run.routing` section of your `zerops.yaml`: ```yaml title="zerops.yaml" run: + base: alpine/static routing: + root: dist + cors: "*" redirects: - # Only needed for custom redirect scenarios - from: /special-path/* to: /specific-landing-page status: 302 + headers: + - for: "/*" + values: + X-Frame-Options: "'DENY'" ``` +Zerops turns this into `location` blocks inside the generated Nginx configuration. Every deploy regenerates the configuration, so `routing` changes take effect on the next deploy. + +### Path matching + +The `from` field of a redirect and the `for` field of a header rule use the same matching rules: + +- **Without a wildcard** (`/about`) the rule matches that exact path only. `/about?x=1` matches (the query string is not part of the path), `/about/` and `/about/team` do not. +- **With a trailing `*`** (`/blog/*`) the rule matches the path prefix. `/blog/`, `/blog/post` and `/blog/2024/post.html` all match. `/blog` without the trailing slash does not. +- `*` is only supported at the end of a path. Patterns such as `/*.html` or `/*/edit` are not supported. +- When several rules match, an exact rule wins over a prefix rule, and the longest matching prefix wins among prefix rules. +- Path rules apply to paths only. Redirects between domains use [absolute redirects](#absolute-redirects), which are evaluated before any path rule. + +`/*` matches everything and is the rule to use when you want a header on all responses. + ### Redirects #### Relative Redirects @@ -65,7 +141,7 @@ run: Remember that SPA routing is already built into the default behavior. You don't need to add any custom redirects for client-side routing to work. ::: -Use relative redirects to route paths within your application. When both `from` and `to` are relative paths, you can omit the `status` code to create a masked redirect that shows the content of the target page while preserving the original URL: +When both `from` and `to` are paths, the redirect is relative. Omitting `status` creates a **masked redirect**: the content of the target is served while the URL in the browser stays the same. With a `status`, the browser receives an HTTP redirect to the target: ```yaml title="zerops.yaml" routing: @@ -93,35 +169,56 @@ routing: status: 302 ``` -:::caution Important -When using `preservePath` with wildcards, ensure the `to` path ends with a `/` to maintain proper path concatenation. For example, `/blog/*` to `/new-blog/` will correctly redirect `/blog/hello.html` to `/new-blog/hello.html`, while `/new-blog` would result in `/new-bloghello.html`. -::: +- `status` can be any redirect code, typically `301`, `302`, `307` or `308`. +- `preservePath` appends the part of the path after the wildcard to `to`. `/blog/*` to `/articles/` redirects `/blog/hello.html` to `/articles/hello.html`. End `to` with a `/`, otherwise the result is `/articleshello.html`. +- `preserveQuery` appends the original query string. Without it the query string is dropped. +- A masked redirect serves the target through the [default rules](#how-requests-are-served), so `to: /about-us` may resolve to `about-us.html` or `about-us/index.html`. +- `preservePath` and `preserveQuery` are not allowed on masked redirects. Setting them fails the deploy with `Preserve path must not be set for masked redirects`. A masked redirect for a prefix (`from: /legacy/*`) simply serves the same `to` for every matching path. #### Absolute Redirects -For redirecting between domains or to external URLs, use absolute redirects by including `http://` or `https://`. When using absolute URLs in either `from` or `to`, you must specify a `status` code: +Use absolute URLs (`http://` or `https://`) to redirect between domains or to an external site. Absolute redirects require a `status`. + +A redirect with an **absolute `to`** and a path `from` works like a relative redirect, only the destination is external. Any status code is allowed: ```yaml title="zerops.yaml" routing: redirects: - # Redirect an old domain to a new one + # /docs/getting-started -> https://docs.example.com/getting-started + - from: /docs/* + to: https://docs.example.com/ + status: 301 + preservePath: true +``` + +A redirect with an **absolute `from`** matches on the domain of the request, which is why it only makes sense for domains that are [pointed at this service](/references/networking/public-access#custom-domain-access). It is evaluated before any path rule and supports `status` `301` or `302` only: + +```yaml title="zerops.yaml" +routing: + redirects: + # Redirect an old domain to a new one, keeping the query string - from: https://old-domain.com/* to: https://new-domain.com status: 301 - preserveQuery: true # Optional: maintain query parameters + preserveQuery: true - # Redirect with path preservation + # Redirect with path preservation: https://old-site.com/blog/x -> https://new-site.com/blog/x - from: https://old-site.com/* - to: https://new-site.com/ + to: https://new-site.com status: 301 preservePath: true ``` +- With an absolute `from`, `preservePath` appends the complete request path (it always starts with `/`), so do **not** end `to` with a `/`. +- The domain match is a case-insensitive substring match on `host + path`. `https://old-domain.com/*` therefore also matches `www.old-domain.com` and every other subdomain of `old-domain.com`. +- `https://*.old-domain.com/*` matches subdomains of `old-domain.com` only, not `old-domain.com` itself. +- `https://old-domain.com/` without the trailing `*` matches the root path of that domain only. + #### Wildcard Matching -Use `*` as a wildcard in your paths: -- **At the end of a path**: Matches any subsequent content -- **At the start of a domain** (after `https://`): Enables regex matching for subdomains +Use `*` as a wildcard: +- **At the end of a path** it matches the path prefix, see [path matching](#path-matching). +- **At the start of a domain** in an absolute `from` (`https://*.domain.com/*`) it matches all subdomains. Example of domain management: @@ -129,12 +226,12 @@ Example of domain management: run: routing: redirects: - # Redirect a specific domain to an article + # Redirect a specific domain (and its subdomains) to an article - from: https://promo-domain.com/* to: https://main-site.com/special-offer status: 302 - # Redirect all subdomains to main site + # Redirect only subdomains of old-domain.com to the main site - from: https://*.old-domain.com/* to: https://main-site.com status: 302 @@ -142,11 +239,12 @@ run: #### Matching Priority -When multiple redirects are configured, they follow Nginx's matching priority system: +Rules are matched in this order: -1. Exact matches are checked first -2. Simple path matches (without wildcards) are checked next -3. Pattern matches (with wildcards) are checked last +1. Absolute redirects (matched on the request domain) +2. Exact path rules (`from` without a wildcard) +3. Prefix rules (`from` ending with `*`), longest prefix first +4. The [default behavior](#how-requests-are-served) for everything else For example: @@ -158,11 +256,11 @@ routing: to: /home status: 302 - # Simple path match - masked redirect + # Exact match - masked redirect - from: /about to: /about-us - # Pattern match with path preservation + # Prefix match with path preservation - from: /blog/* to: /articles/ preservePath: true @@ -170,10 +268,11 @@ routing: ``` In this configuration: -- `/` will redirect to `/home` with a 302 status -- `/about` will show content from `/about-us` but keep the URL as `/about` -- `/blog/post-123.html` will redirect to `/articles/post-123.html` -- Any other path will use the [default behavior](#default-behavior) +- `/` redirects to `/home` with a 302 status +- `/about` shows content from `/about-us` but keeps the URL as `/about` +- `/about/` and `/about/team` do not match the exact rule and use the default behavior +- `/blog/post-123.html` redirects to `/articles/post-123.html` +- Any other path uses the [default behavior](#how-requests-are-served) #### Common Redirect Patterns @@ -184,8 +283,10 @@ routing: - from: https://old-domain.com/* to: https://new-domain.com status: 301 + preservePath: true + preserveQuery: true ``` -Use permanent (301) redirects when permanently moving content to maintain SEO value. +Use permanent (301) redirects when permanently moving content to maintain SEO value. Both `preservePath` and `preserveQuery` keep the visitor on the same page of the new domain. **Multiple Domain Management** ```yaml title="zerops.yaml" @@ -202,12 +303,23 @@ run: to: https://main-site.com/campaign status: 302 - # Legacy domains and subdomains + # Legacy subdomains - from: https://*.legacy-domain.com/* to: https://main-site.com status: 302 ``` +**Moving a section of the site** +```yaml title="zerops.yaml" +routing: + redirects: + - from: /blog/* + to: /articles/ + status: 301 + preservePath: true + preserveQuery: true +``` + ### CORS Configuration You can enable CORS for your static service by adding a `cors` directive: @@ -222,25 +334,27 @@ run: cors: "'*' always" ``` -The `cors` directive sets the following headers: +The `cors` directive sets the following headers on every response, including redirects: - `Access-Control-Allow-Origin` - `Access-Control-Allow-Methods` - `Access-Control-Allow-Headers` - `Access-Control-Expose-Headers` +All four headers receive the same value. If you need different values per header, set them individually with the [`headers`](#custom-headers) directive instead. + :::note The `cors` directive has a special case: if you specify just `"*"`, it's automatically converted to `'*'`. For any other values, you need to include the proper Nginx syntax including quotes. ::: ### Custom Headers -For more control over HTTP headers, use the `headers` directive: +For more control over HTTP headers, use the `headers` directive. The `for` field uses the same [path matching](#path-matching) as redirects, so use `"/*"` to cover the whole site. `"/"` alone would match the homepage only: ```yaml title="zerops.yaml" run: routing: headers: - - for: "/" + - for: "/*" values: # All values need proper quoting since they're inserted directly into Nginx X-Frame-Options: "'DENY'" @@ -262,7 +376,7 @@ Here are examples for different header scenarios: ```yaml title="zerops.yaml" headers: - - for: "/" + - for: "/*" values: # Simple header with proper quoting X-Frame-Options: "'DENY'" @@ -290,9 +404,12 @@ add_header Content-Security-Policy "default-src 'self' https://cdn.example.com; ``` :::important Path Handling -When you specify headers for a path that doesn't have an existing location block, the Static service automatically creates a location with the same [default behavior](#default-behavior) as the root path (trying files in order: `$uri`, `$uri.html`, `$uri/index.html`, `/index.html` or returning 404). +Headers are attached to the matched location, and only the single best-matching rule applies to a request. Rules are not merged: -If you add headers for a path that already has a location block, your headers will be merged with the existing configuration. +- A request for `/docs/page` with rules for `/*` and `/docs/*` receives only the `/docs/*` headers. Repeat the site-wide headers in the more specific rule if you need both. +- A redirect defined in `redirects` does not pick up headers from a broader rule such as `/*`, only the [`cors`](#cors-configuration) headers. To add headers to a redirect response, add a header rule with the same `for` path as the redirect's `from`. The two are merged. +- A header rule for a path without a redirect serves files with the same [default behavior](#how-requests-are-served) as the rest of the site. +- Without `always`, Nginx adds a header only to 2xx, 3xx and 304 responses, so use `always` for headers that must be present on error pages too. ::: ### Combining CORS and Custom Headers @@ -304,7 +421,7 @@ run: routing: cors: "'*' always" headers: - - for: "/" + - for: "/*" values: X-Frame-Options: "'DENY'" ``` @@ -319,12 +436,14 @@ If you specify Access-Control headers in the `headers` directive, they will over Single Page Applications render content with JavaScript, which most crawlers can't process—they see an empty page instead of your content. This affects traditional search engines, social media platforms, and AI tools like ChatGPT, Perplexity, and Claude. -The Static service includes built-in support for Prerender.io, which automatically detects crawlers and serves them pre-rendered HTML while your users get the full interactive experience. +The Static service includes built-in support for [Prerender.io](https://prerender.io), which automatically detects crawlers (search engines, social media link previews, SEO tools and AI bots) and serves them pre-rendered HTML while your users get the full interactive experience. Requests for assets such as scripts, styles and images are never prerendered. ### Setup -1. Set the `PRERENDER_TOKEN` secret variable with your Prerender.io token -2. The service automatically configures necessary rewrites based on user agents +1. Set the `PRERENDER_TOKEN` [secret variable](/features/env-variables) with your Prerender.io token +2. Restart the service (or trigger a new deploy). The Nginx configuration is generated when a container starts, so it picks the token up on the next start + +No changes to `zerops.yaml` are needed. ### Custom Prerender Host @@ -333,7 +452,7 @@ If you're using a custom Prerender host, add it to environment variables in `zer ```yaml title="zerops.yaml" run: envVariables: - - PRERENDER_HOST=your.prerender.host + PRERENDER_HOST: your.prerender.host ``` :::note Default @@ -355,10 +474,10 @@ zerops: deployFiles: - dist/~ # Your framework's output directory run: - base: static + base: alpine/static ``` -The key is pointing `deployFiles` to wherever your framework outputs its built files (`dist/`, `build/`, `.output/public/`, etc.). +The key is pointing `deployFiles` to wherever your framework outputs its built files (`dist/`, `build/`, `.output/public/`, etc.). The trailing `/~` deploys the folder's contents to the document root. This configuration: 1. Uses Node.js for building the application @@ -374,6 +493,20 @@ For framework-specific examples, check out our [recipe collection](https://githu ## Advanced Topics +### Custom Nginx configuration + +The Static service also accepts your own Nginx configuration through `run.siteConfigPath`, the same attribute the [Nginx service](/nginx/how-to/customize-web-server) uses. The configuration is chosen with this precedence: + +1. `run.routing` is set: the configuration is generated from it and `siteConfigPath` is ignored +2. Only `run.siteConfigPath` is set: your file is used as the complete `server` configuration +3. Neither is set: the generated default configuration is used + +A `.tmpl` file is rendered as a Go template with `{{.DocumentRoot}}` (the value of `run.documentRoot`, `/var/www` when unset) and `{{.Environment.NAME}}` for environment variables. Any other extension is copied verbatim. See the [Nginx service guide](/nginx/how-to/customize-web-server#customize-nginx-configuration) for the requirements a custom configuration must meet. + +:::tip +If you find yourself writing a custom configuration, consider switching to the [Nginx service](/nginx/overview). It is the same Nginx with `documentRoot` and `siteConfigPath` as first-class options, and it is what the rest of the documentation assumes for hand-written configurations. +::: + ### Switching to Full Nginx If you need more control over your Nginx configuration: @@ -384,6 +517,10 @@ If you need more control over your Nginx configuration: 4. Copy the generated Nginx configuration 5. Use this configuration as a starting point for a full Nginx service +The same file is available inside any running container of the service at `/etc/nginx/sites-enabled/default.site`, for example over [SSH](/references/networking/ssh). + +To migrate, change `run.base` to `alpine/nginx@latest` (or `ubuntu/nginx@latest`), replace `run.routing` with `run.siteConfigPath` pointing at the copied configuration (with the `root` directive adjusted or replaced by `{{.DocumentRoot}}`), and remove `routing`, since the Nginx service ignores it. Prerender.io support is part of the generated configuration and will be carried over with it. + :::tip This allows you to move to a more customizable setup while maintaining your existing routing logic. ::: @@ -406,7 +543,7 @@ run: to: https://main-site.com/campaign status: 302 - # Legacy domains and subdomains + # Legacy subdomains - from: https://*.legacy-domain.com/* to: https://main-site.com status: 302 @@ -416,7 +553,7 @@ run: ### Development Setup -Configuration for local development with CORS and API proxying: +Configuration for a development environment with CORS and an API on another domain: ```yaml title="zerops.yaml" run: @@ -424,12 +561,18 @@ run: # CORS with proper quoting cors: "'*' always" redirects: - # API requests + # Send browsers calling /api/... to the API domain - from: /api/* - to: https://api.your-domain.com - status: 302 + to: https://api.your-domain.com/ + status: 307 + preservePath: true + preserveQuery: true ``` +:::note +This is a browser redirect, not a reverse proxy. The Static service cannot proxy requests. To serve an API under the same domain as the frontend, use [domain access routing](/references/networking/public-access#http-routing-setup) or the [Nginx service](/nginx/overview) with a `proxy_pass` configuration. +::: + ### Production Setup with Security Security-enhanced configuration for production environments: @@ -438,11 +581,11 @@ Security-enhanced configuration for production environments: run: routing: headers: - # Custom headers for default location + # Custom headers for the whole site - for: "/*" values: X-Frame-Options: "'DENY' always" X-Content-Type-Options: "'nosniff' always" # Note the proper escaping of single quotes Content-Security-Policy: '"default-src ''self''" always' -``` \ No newline at end of file +``` diff --git a/apps/docs/content/zerops-yaml/specification.mdx b/apps/docs/content/zerops-yaml/specification.mdx index 7c723afd..f58cde9d 100644 --- a/apps/docs/content/zerops-yaml/specification.mdx +++ b/apps/docs/content/zerops-yaml/specification.mdx @@ -140,10 +140,7 @@ build: ### os -Sets the operating system for the build environment. Options: - -- `alpine` (default) -- `ubuntu` (default for ubuntu service) +Deprecated. The operating system is part of the `base` value, for example `ubuntu/nodejs@22` or `alpine/nodejs@22`. Use the OS-prefixed form in both `build.base` and `run.base` instead of setting `os`. A bare `nodejs@22` defaults to Alpine. Current versions: @@ -152,7 +149,7 @@ Current versions: ```yaml build: - os: ubuntu + base: ubuntu/nodejs@22 ``` ### prepareCommands @@ -355,7 +352,7 @@ run: ### os -Sets the operating system for the runtime environment. Options and versions are the same as for the build environment. +Deprecated, same as for the build environment: put the OS into `run.base` (`ubuntu/nodejs@22`) instead. ### ports @@ -460,11 +457,23 @@ See [start-commands-example](https://github.com/zeropsio/start-commands-example) ### documentRoot -Customizes the root folder for publicly accessible web server content (available only for webserver runtimes). +Customizes the root folder for publicly accessible web server content. The path is relative to `/var/www`. Available for the [Nginx](/nginx/how-to/build-pipeline#documentroot) and [PHP](/php/how-to/build-pipeline#documentroot) services. The [Static service](/static/overview#document-root) ignores it, use `routing.root` there. + +```yaml +run: + base: alpine/nginx@latest + documentRoot: dist +``` ### siteConfigPath -Sets the custom webserver configuration (available only for webserver runtimes). +Sets the custom webserver configuration for the [Nginx](/nginx/how-to/customize-web-server), [PHP](/php/how-to/customize-web-server) and [Static](/static/overview#custom-nginx-configuration) services. The path is relative to `/var/www` and the file must be part of the deployed files. A `.tmpl` file is rendered as a template with `{{.DocumentRoot}}` and `{{.Environment.NAME}}`, any other file is used verbatim. On the Static service `routing` takes precedence: when both are set, `siteConfigPath` is ignored. + +```yaml +run: + base: alpine/nginx@latest + siteConfigPath: site_config.tmpl +``` ### envVariables @@ -548,12 +557,13 @@ The placeholder gets replaced with the actual JWT public key during deployment. ### routing -Configures URL routing, redirects, and HTTP headers (only for Static services). +Configures the document root, redirects, CORS and HTTP headers of the [Static service](/static/overview#routing--configuration). Any other service, including the Nginx service, ignores this section without an error. ```yaml run: + base: alpine/static routing: - root: /custom/root + root: dist cors: "'*' always" redirects: - from: /old-path @@ -568,31 +578,31 @@ run: Available parameters: #### root -Sets a custom root directory for the service. +Sets the folder served by the service, relative to `/var/www` (default `/var/www` itself). This is the Static service's equivalent of `documentRoot`. - Type: `string` #### cors Enables CORS headers for cross-origin requests. - Type: `string` -- Sets `Access-Control-Allow-Origin`, `Access-Control-Allow-Methods`, `Access-Control-Allow-Headers`, and `Access-Control-Expose-Headers` +- Sets `Access-Control-Allow-Origin`, `Access-Control-Allow-Methods`, `Access-Control-Allow-Headers`, and `Access-Control-Expose-Headers`, all to the same value - Special case: `"*"` is automatically converted to `'*'` #### redirects Defines URL redirects and rewrites. - Type: `array of objects` - Each redirect object supports: - - **from** - Source path to match ([supports wildcards](/static/overview#wildcard-matching) with `*`) - - **to** - Destination path - - **status** - HTTP status code (required for absolute URLs) - - **preservePath** - Preserve path after wildcard match - - **preserveQuery** - Preserve query parameters + - **from** - Source path to match. Without `*` it matches the exact path, with a trailing `*` it matches the path prefix ([path matching](/static/overview#path-matching)). An absolute URL (`https://old-domain.com/*`) matches on the request domain. + - **to** - Destination path or absolute URL + - **status** - HTTP status code. Omit it for a masked redirect that serves the target content under the original URL. Required for absolute URLs, and limited to `301` or `302` when `from` is absolute. + - **preservePath** - Append the part of the path after the wildcard to `to`. Not allowed on masked redirects. + - **preserveQuery** - Append the original query string. Not allowed on masked redirects. #### headers Sets custom HTTP headers for specific paths. - Type: `array of objects` - Each header object supports: - - **for** - Path pattern to match - - **values** - Object with header name/value pairs + - **for** - Path to match, same rules as `from` (`"/*"` for the whole site, `"/"` matches the homepage only) + - **values** - Object with header name/value pairs. Values are inserted into `add_header` verbatim, so include the quotes: `X-Frame-Options: "'DENY'"` **Example usage:** diff --git a/apps/docs/static/llms-full.txt b/apps/docs/static/llms-full.txt index 99d33e92..b593aed6 100644 --- a/apps/docs/static/llms-full.txt +++ b/apps/docs/static/llms-full.txt @@ -268,10 +268,7 @@ build: ### os *[Optional]* -Sets the operating system for the build environment. Options: - -- `alpine` (default) -- `ubuntu` (default for ubuntu service) +Deprecated. The operating system is part of the `base` value, for example `ubuntu/nodejs@22` or `alpine/nodejs@22`. Use the OS-prefixed form in both `build.base` and `run.base` instead of setting `os`. A bare `nodejs@22` defaults to Alpine. Current versions: @@ -280,7 +277,7 @@ Current versions: ```yaml build: - os: ubuntu + base: ubuntu/nodejs@22 ``` ### prepareCommands *[Optional]* @@ -482,7 +479,7 @@ run: ### os *[Optional]* -Sets the operating system for the runtime environment. Options and versions are the same as for the build environment. +Deprecated, same as for the build environment: put the OS into `run.base` (`ubuntu/nodejs@22`) instead. ### ports *[Optional]* @@ -565,15 +562,45 @@ run: - litestream restore -if-replica-exists -if-db-not-exists -config=litestream.yaml $DB_NAME ``` +Each entry supports: + +- `command` (required) - the command to run +- `name` (optional) - distinguishes the process in logs +- `workingDir` (optional, default `/var/www`) - the directory the command and its `initCommands` run in +- `user` (optional, default `zerops`) - the system user the command and its `initCommands` run under. The user has to exist in the runtime container, create it in `prepareCommands`. +- `initCommands` (optional) - commands run before this process starts, each time a container starts or restarts + +```yaml +run: + prepareCommands: + - sudo adduser --system --group --home /home/git git + startCommands: + - command: gitea web + name: gitea + user: git +``` + See [start-commands-example](https://github.com/zeropsio/start-commands-example) ### documentRoot *[Optional]* -Customizes the root folder for publicly accessible web server content (available only for webserver runtimes). +Customizes the root folder for publicly accessible web server content. The path is relative to `/var/www`. Available for the [Nginx](/nginx/how-to/build-pipeline#documentroot) and [PHP](/php/how-to/build-pipeline#documentroot) services. The [Static service](/static/overview#document-root) ignores it, use `routing.root` there. + +```yaml +run: + base: alpine/nginx@latest + documentRoot: dist +``` ### siteConfigPath *[Optional]* -Sets the custom webserver configuration (available only for webserver runtimes). +Sets the custom webserver configuration for the [Nginx](/nginx/how-to/customize-web-server), [PHP](/php/how-to/customize-web-server) and [Static](/static/overview#custom-nginx-configuration) services. The path is relative to `/var/www` and the file must be part of the deployed files. A `.tmpl` file is rendered as a template with `{{.DocumentRoot}}` and `{{.Environment.NAME}}`, any other file is used verbatim. On the Static service `routing` takes precedence: when both are set, `siteConfigPath` is ignored. + +```yaml +run: + base: alpine/nginx@latest + siteConfigPath: site_config.tmpl +``` ### envVariables *[Optional]* @@ -619,6 +646,10 @@ Files or directories to process for variable replacement. Directory targets only process files directly in the specified directory, not subdirectories for performance reasons. To process files in subdirectories, specify each subdirectory explicitly in the target array. For example, ./config/ processes only files in the config directory itself, not files in ./config/jwt/ or other subdirectories. ::: +:::info +Not to be confused with the [`zsc env-replace`](/references/zsc#env-replace) command, which renders `{{.VARIABLE}}` Go templates from a source path into a separate target path and can be run from `initCommands` or manually. +::: + **How it works:** 1. Define placeholders in your files using the specified delimiters 2. Set environment variables with matching names @@ -653,12 +684,13 @@ The placeholder gets replaced with the actual JWT public key during deployment. ### routing *[Optional]* -Configures URL routing, redirects, and HTTP headers (only for Static services). +Configures the document root, redirects, CORS and HTTP headers of the [Static service](/static/overview#routing--configuration). Any other service, including the Nginx service, ignores this section without an error. ```yaml run: + base: alpine/static routing: - root: /custom/root + root: dist cors: "'*' always" redirects: - from: /old-path @@ -673,31 +705,31 @@ run: Available parameters: #### root *[Optional]* -Sets a custom root directory for the service. +Sets the folder served by the service, relative to `/var/www` (default `/var/www` itself). This is the Static service's equivalent of `documentRoot`. - Type: `string` #### cors *[Optional]* Enables CORS headers for cross-origin requests. - Type: `string` -- Sets `Access-Control-Allow-Origin`, `Access-Control-Allow-Methods`, `Access-Control-Allow-Headers`, and `Access-Control-Expose-Headers` +- Sets `Access-Control-Allow-Origin`, `Access-Control-Allow-Methods`, `Access-Control-Allow-Headers`, and `Access-Control-Expose-Headers`, all to the same value - Special case: `"*"` is automatically converted to `'*'` #### redirects *[Optional]* Defines URL redirects and rewrites. - Type: `array of objects` - Each redirect object supports: - - **from** *[Required]* - Source path to match ([supports wildcards](/static/overview#wildcard-matching) with `*`) - - **to** *[Required]* - Destination path - - **status** *[Optional]* - HTTP status code (required for absolute URLs) - - **preservePath** *[Optional]* - Preserve path after wildcard match - - **preserveQuery** *[Optional]* - Preserve query parameters + - **from** *[Required]* - Source path to match. Without `*` it matches the exact path, with a trailing `*` it matches the path prefix ([path matching](/static/overview#path-matching)). An absolute URL (`https://old-domain.com/*`) matches on the request domain. + - **to** *[Required]* - Destination path or absolute URL + - **status** *[Optional]* - HTTP status code. Omit it for a masked redirect that serves the target content under the original URL. Required for absolute URLs, and limited to `301` or `302` when `from` is absolute. + - **preservePath** *[Optional]* - Append the part of the path after the wildcard to `to`. Not allowed on masked redirects. + - **preserveQuery** *[Optional]* - Append the original query string. Not allowed on masked redirects. #### headers *[Optional]* Sets custom HTTP headers for specific paths. - Type: `array of objects` - Each header object supports: - - **for** *[Required]* - Path pattern to match - - **values** *[Required]* - Object with header name/value pairs + - **for** *[Required]* - Path to match, same rules as `from` (`"/*"` for the whole site, `"/"` matches the homepage only) + - **values** *[Required]* - Object with header name/value pairs. Values are inserted into `add_header` verbatim, so include the quotes: `X-Frame-Options: "'DENY'"` **Example usage:** @@ -5180,7 +5212,7 @@ Stuck, or want to share what you built? Our core team and community are on Disco # Static > Overview -The Static service provides a way to serve static content through a pre-configured Nginx setup. It balances simplicity with the flexibility needed for modern web applications. +The Static service serves static files (HTML, CSS, JavaScript, images, the build output of any frontend framework) through a pre-configured Nginx. You describe redirects, headers and CORS declaratively in `zerops.yaml` and Zerops generates the Nginx configuration for you. ### Experience the simplicity of Zerops @@ -5188,49 +5220,126 @@ Deploy an Analog app with static hosting in seconds. All you need is a Zerops ac [Deploy "analog" recipe on Zerops](https://app.zerops.io/recipe/?lf=analog) +## Static or Nginx service? + +Zerops offers two services for static content. Both run Nginx, but they are configured differently and support different `zerops.yaml` attributes: + +| | Static service | [Nginx service](/nginx/overview) | +|---|---|---| +| `run.base` | `alpine/static` or `ubuntu/static` | `alpine/nginx@latest` or `ubuntu/nginx@latest` | +| Nginx configuration | Generated from `run.routing` | Built-in default, or your own file via `run.siteConfigPath` | +| Redirects, custom headers, CORS | Declarative `run.routing` | Written by you in the Nginx configuration | +| Fallback for missing paths | `/index.html` (SPA friendly), plus extensionless `.html` pages | `/index.html` | +| Prerender.io for crawlers | Built in, enabled by `PRERENDER_TOKEN` | Not built in | +| Reverse proxy (`proxy_pass`), caching, rate limiting, other ports | Not available | Anything Nginx can do, in your own configuration | +| Document root | `run.routing.root` | `run.documentRoot` | +| Document root | `run.routing.root` | `run.documentRoot` | + +Choose **Static** when you deploy framework build output or plain files and need at most redirects, headers and CORS. Choose **Nginx** when you need Nginx directives that `routing` cannot express, typically proxying to another service, response caching, or listening on additional ports. + +:::caution Attributes are not interchangeable +- `run.routing` is read only by the Static service. On any other service, including Nginx, it is silently ignored. +- `run.documentRoot` is ignored by the Static service. Use `run.routing.root` instead. +::: + +:::tip Sending paths to another service +You do not need an Nginx service just to send `/api` to a backend. [Domain access routing](/references/networking/public-access#http-routing-setup) on the project's L7 balancer maps public paths to services and ports, so a Static frontend and an API can share one domain. +::: + ## Quick Start -Add a Static service to your project by including this in your `zerops.yaml`: +Build your frontend with any runtime and hand the output to the Static service: ```yaml title="zerops.yaml" zerops: - setup: app + build: + base: nodejs@latest + buildCommands: + - npm i + - npm run build + deployFiles: + - dist/~ # deploy the *contents* of dist to /var/www run: - os: alpine - base: static + base: alpine/static ``` -## Routing & Configuration +The `~` in `dist/~` deploys the contents of `dist` directly into `/var/www`, so `index.html` ends up at the document root. If you deploy the folder as a whole (`deployFiles: - dist`), point the service at it with [`routing.root: dist`](#document-root). -### Default Behavior +If your files are already built, skip the `build` section and [deploy them with zCLI](/references/cli). + +The base carries the operating system: `alpine/static` or `ubuntu/static`. The bare shorthand `static` is accepted and means `alpine/static`. + +## How requests are served -Every Static service in Zerops comes with built-in defaults optimized for modern web applications, including Single Page Applications. By default, for any incoming request, the service will: -1. Try to serve the exact path (`$uri`) -2. Try with .html extension (`$uri.html`) -3. Look for an index.html in the directory (`$uri/index.html`) -4. Fall back to `/index.html` (suitable for SPAs) -5. Return 404 if none of the above exist +The generated configuration serves files from the [document root](#document-root) and resolves every request in this order: + +1. The exact path (`$uri`) +2. The path with `.html` appended (`$uri.html`), so `/about` serves `about.html` +3. `index.html` inside the directory (`$uri/index.html`), so `/docs` serves `docs/index.html` +4. `/index.html`, which makes client-side routing of Single Page Applications work +5. HTTP 404 if `/index.html` does not exist either + +Other built-in behavior: + +- The service listens on port 80 only. HTTPS is terminated on the Zerops balancer and forwarded as plain HTTP. +- Everything under `/.git` returns 404. +- Gzip compression is enabled for text-based content types. +- Prerender.io is wired in and becomes active once `PRERENDER_TOKEN` is set, see [SEO with Prerender](#seo-with-prerender). :::important SPAs -Single Page Applications work out of the box without any additional configuration. The built-in fallback to `/index.html` ensures that client-side routing functions properly. +Single Page Applications work out of the box. No redirects are needed for client-side routing. A consequence of the fallback is that a request for a nonexistent path returns `/index.html` with status 200, not 404. ::: -### Custom Routing Configuration +## Document root -The Static service allows you to configure additional URL routing and redirects through simple YAML configuration, abstracting away the complexity of Nginx configuration. +By default files are served from `/var/www`, the folder your `deployFiles` land in. To serve a subfolder, set `run.routing.root`. The path is relative to `/var/www`: + +```yaml title="zerops.yaml" +run: + base: alpine/static + routing: + root: dist # serves /var/www/dist +``` -Configure custom routing beyond the default behavior in the `run.routing` section of your `zerops.yaml`: +:::caution +`run.documentRoot` has no effect on the Static service. It is only used by the [Nginx](/nginx/how-to/build-pipeline#documentroot) and PHP services (and inside a [custom `.tmpl` configuration](#custom-nginx-configuration)). +::: + +## Routing & Configuration + +Configure redirects, headers and CORS in the `run.routing` section of your `zerops.yaml`: ```yaml title="zerops.yaml" run: + base: alpine/static routing: + root: dist + cors: "*" redirects: - # Only needed for custom redirect scenarios - from: /special-path/* to: /specific-landing-page status: 302 + headers: + - for: "/*" + values: + X-Frame-Options: "'DENY'" ``` +Zerops turns this into `location` blocks inside the generated Nginx configuration. Every deploy regenerates the configuration, so `routing` changes take effect on the next deploy. + +### Path matching + +The `from` field of a redirect and the `for` field of a header rule use the same matching rules: + +- **Without a wildcard** (`/about`) the rule matches that exact path only. `/about?x=1` matches (the query string is not part of the path), `/about/` and `/about/team` do not. +- **With a trailing `*`** (`/blog/*`) the rule matches the path prefix. `/blog/`, `/blog/post` and `/blog/2024/post.html` all match. `/blog` without the trailing slash does not. +- `*` is only supported at the end of a path. Patterns such as `/*.html` or `/*/edit` are not supported. +- When several rules match, an exact rule wins over a prefix rule, and the longest matching prefix wins among prefix rules. +- Path rules apply to paths only. Redirects between domains use [absolute redirects](#absolute-redirects), which are evaluated before any path rule. + +`/*` matches everything and is the rule to use when you want a header on all responses. + ### Redirects #### Relative Redirects @@ -5239,7 +5348,7 @@ run: Remember that SPA routing is already built into the default behavior. You don't need to add any custom redirects for client-side routing to work. ::: -Use relative redirects to route paths within your application. When both `from` and `to` are relative paths, you can omit the `status` code to create a masked redirect that shows the content of the target page while preserving the original URL: +When both `from` and `to` are paths, the redirect is relative. Omitting `status` creates a **masked redirect**: the content of the target is served while the URL in the browser stays the same. With a `status`, the browser receives an HTTP redirect to the target: ```yaml title="zerops.yaml" routing: @@ -5267,35 +5376,56 @@ routing: status: 302 ``` -:::caution Important -When using `preservePath` with wildcards, ensure the `to` path ends with a `/` to maintain proper path concatenation. For example, `/blog/*` to `/new-blog/` will correctly redirect `/blog/hello.html` to `/new-blog/hello.html`, while `/new-blog` would result in `/new-bloghello.html`. -::: +- `status` can be any redirect code, typically `301`, `302`, `307` or `308`. +- `preservePath` appends the part of the path after the wildcard to `to`. `/blog/*` to `/articles/` redirects `/blog/hello.html` to `/articles/hello.html`. End `to` with a `/`, otherwise the result is `/articleshello.html`. +- `preserveQuery` appends the original query string. Without it the query string is dropped. +- A masked redirect serves the target through the [default rules](#how-requests-are-served), so `to: /about-us` may resolve to `about-us.html` or `about-us/index.html`. +- `preservePath` and `preserveQuery` are not allowed on masked redirects. Setting them fails the deploy with `Preserve path must not be set for masked redirects`. A masked redirect for a prefix (`from: /legacy/*`) simply serves the same `to` for every matching path. #### Absolute Redirects -For redirecting between domains or to external URLs, use absolute redirects by including `http://` or `https://`. When using absolute URLs in either `from` or `to`, you must specify a `status` code: +Use absolute URLs (`http://` or `https://`) to redirect between domains or to an external site. Absolute redirects require a `status`. + +A redirect with an **absolute `to`** and a path `from` works like a relative redirect, only the destination is external. Any status code is allowed: ```yaml title="zerops.yaml" routing: redirects: - # Redirect an old domain to a new one + # /docs/getting-started -> https://docs.example.com/getting-started + - from: /docs/* + to: https://docs.example.com/ + status: 301 + preservePath: true +``` + +A redirect with an **absolute `from`** matches on the domain of the request, which is why it only makes sense for domains that are [pointed at this service](/references/networking/public-access#custom-domain-access). It is evaluated before any path rule and supports `status` `301` or `302` only: + +```yaml title="zerops.yaml" +routing: + redirects: + # Redirect an old domain to a new one, keeping the query string - from: https://old-domain.com/* to: https://new-domain.com status: 301 - preserveQuery: true # Optional: maintain query parameters + preserveQuery: true - # Redirect with path preservation + # Redirect with path preservation: https://old-site.com/blog/x -> https://new-site.com/blog/x - from: https://old-site.com/* - to: https://new-site.com/ + to: https://new-site.com status: 301 preservePath: true ``` +- With an absolute `from`, `preservePath` appends the complete request path (it always starts with `/`), so do **not** end `to` with a `/`. +- The domain match is a case-insensitive substring match on `host + path`. `https://old-domain.com/*` therefore also matches `www.old-domain.com` and every other subdomain of `old-domain.com`. +- `https://*.old-domain.com/*` matches subdomains of `old-domain.com` only, not `old-domain.com` itself. +- `https://old-domain.com/` without the trailing `*` matches the root path of that domain only. + #### Wildcard Matching -Use `*` as a wildcard in your paths: -- **At the end of a path**: Matches any subsequent content -- **At the start of a domain** (after `https://`): Enables regex matching for subdomains +Use `*` as a wildcard: +- **At the end of a path** it matches the path prefix, see [path matching](#path-matching). +- **At the start of a domain** in an absolute `from` (`https://*.domain.com/*`) it matches all subdomains. Example of domain management: @@ -5303,12 +5433,12 @@ Example of domain management: run: routing: redirects: - # Redirect a specific domain to an article + # Redirect a specific domain (and its subdomains) to an article - from: https://promo-domain.com/* to: https://main-site.com/special-offer status: 302 - # Redirect all subdomains to main site + # Redirect only subdomains of old-domain.com to the main site - from: https://*.old-domain.com/* to: https://main-site.com status: 302 @@ -5316,11 +5446,12 @@ run: #### Matching Priority -When multiple redirects are configured, they follow Nginx's matching priority system: +Rules are matched in this order: -1. Exact matches are checked first -2. Simple path matches (without wildcards) are checked next -3. Pattern matches (with wildcards) are checked last +1. Absolute redirects (matched on the request domain) +2. Exact path rules (`from` without a wildcard) +3. Prefix rules (`from` ending with `*`), longest prefix first +4. The [default behavior](#how-requests-are-served) for everything else For example: @@ -5332,11 +5463,11 @@ routing: to: /home status: 302 - # Simple path match - masked redirect + # Exact match - masked redirect - from: /about to: /about-us - # Pattern match with path preservation + # Prefix match with path preservation - from: /blog/* to: /articles/ preservePath: true @@ -5344,10 +5475,11 @@ routing: ``` In this configuration: -- `/` will redirect to `/home` with a 302 status -- `/about` will show content from `/about-us` but keep the URL as `/about` -- `/blog/post-123.html` will redirect to `/articles/post-123.html` -- Any other path will use the [default behavior](#default-behavior) +- `/` redirects to `/home` with a 302 status +- `/about` shows content from `/about-us` but keeps the URL as `/about` +- `/about/` and `/about/team` do not match the exact rule and use the default behavior +- `/blog/post-123.html` redirects to `/articles/post-123.html` +- Any other path uses the [default behavior](#how-requests-are-served) #### Common Redirect Patterns @@ -5358,8 +5490,10 @@ routing: - from: https://old-domain.com/* to: https://new-domain.com status: 301 + preservePath: true + preserveQuery: true ``` -Use permanent (301) redirects when permanently moving content to maintain SEO value. +Use permanent (301) redirects when permanently moving content to maintain SEO value. Both `preservePath` and `preserveQuery` keep the visitor on the same page of the new domain. **Multiple Domain Management** ```yaml title="zerops.yaml" @@ -5376,12 +5510,23 @@ run: to: https://main-site.com/campaign status: 302 - # Legacy domains and subdomains + # Legacy subdomains - from: https://*.legacy-domain.com/* to: https://main-site.com status: 302 ``` +**Moving a section of the site** +```yaml title="zerops.yaml" +routing: + redirects: + - from: /blog/* + to: /articles/ + status: 301 + preservePath: true + preserveQuery: true +``` + ### CORS Configuration You can enable CORS for your static service by adding a `cors` directive: @@ -5396,25 +5541,27 @@ run: cors: "'*' always" ``` -The `cors` directive sets the following headers: +The `cors` directive sets the following headers on every response, including redirects: - `Access-Control-Allow-Origin` - `Access-Control-Allow-Methods` - `Access-Control-Allow-Headers` - `Access-Control-Expose-Headers` +All four headers receive the same value. If you need different values per header, set them individually with the [`headers`](#custom-headers) directive instead. + :::note The `cors` directive has a special case: if you specify just `"*"`, it's automatically converted to `'*'`. For any other values, you need to include the proper Nginx syntax including quotes. ::: ### Custom Headers -For more control over HTTP headers, use the `headers` directive: +For more control over HTTP headers, use the `headers` directive. The `for` field uses the same [path matching](#path-matching) as redirects, so use `"/*"` to cover the whole site. `"/"` alone would match the homepage only: ```yaml title="zerops.yaml" run: routing: headers: - - for: "/" + - for: "/*" values: # All values need proper quoting since they're inserted directly into Nginx X-Frame-Options: "'DENY'" @@ -5436,7 +5583,7 @@ Here are examples for different header scenarios: ```yaml title="zerops.yaml" headers: - - for: "/" + - for: "/*" values: # Simple header with proper quoting X-Frame-Options: "'DENY'" @@ -5464,9 +5611,12 @@ add_header Content-Security-Policy "default-src 'self' https://cdn.example.com; ``` :::important Path Handling -When you specify headers for a path that doesn't have an existing location block, the Static service automatically creates a location with the same [default behavior](#default-behavior) as the root path (trying files in order: `$uri`, `$uri.html`, `$uri/index.html`, `/index.html` or returning 404). +Headers are attached to the matched location, and only the single best-matching rule applies to a request. Rules are not merged: -If you add headers for a path that already has a location block, your headers will be merged with the existing configuration. +- A request for `/docs/page` with rules for `/*` and `/docs/*` receives only the `/docs/*` headers. Repeat the site-wide headers in the more specific rule if you need both. +- A redirect defined in `redirects` does not pick up headers from a broader rule such as `/*`, only the [`cors`](#cors-configuration) headers. To add headers to a redirect response, add a header rule with the same `for` path as the redirect's `from`. The two are merged. +- A header rule for a path without a redirect serves files with the same [default behavior](#how-requests-are-served) as the rest of the site. +- Without `always`, Nginx adds a header only to 2xx, 3xx and 304 responses, so use `always` for headers that must be present on error pages too. ::: ### Combining CORS and Custom Headers @@ -5478,7 +5628,7 @@ run: routing: cors: "'*' always" headers: - - for: "/" + - for: "/*" values: X-Frame-Options: "'DENY'" ``` @@ -5493,12 +5643,14 @@ If you specify Access-Control headers in the `headers` directive, they will over Single Page Applications render content with JavaScript, which most crawlers can't process—they see an empty page instead of your content. This affects traditional search engines, social media platforms, and AI tools like ChatGPT, Perplexity, and Claude. -The Static service includes built-in support for Prerender.io, which automatically detects crawlers and serves them pre-rendered HTML while your users get the full interactive experience. +The Static service includes built-in support for [Prerender.io](https://prerender.io), which automatically detects crawlers (search engines, social media link previews, SEO tools and AI bots) and serves them pre-rendered HTML while your users get the full interactive experience. Requests for assets such as scripts, styles and images are never prerendered. ### Setup -1. Set the `PRERENDER_TOKEN` secret variable with your Prerender.io token -2. The service automatically configures necessary rewrites based on user agents +1. Set the `PRERENDER_TOKEN` [secret variable](/features/env-variables) with your Prerender.io token +2. Restart the service (or trigger a new deploy). The Nginx configuration is generated when a container starts, so it picks the token up on the next start + +No changes to `zerops.yaml` are needed. ### Custom Prerender Host @@ -5507,7 +5659,7 @@ If you're using a custom Prerender host, add it to environment variables in `zer ```yaml title="zerops.yaml" run: envVariables: - - PRERENDER_HOST=your.prerender.host + PRERENDER_HOST: your.prerender.host ``` :::note Default @@ -5529,10 +5681,10 @@ zerops: deployFiles: - dist/~ # Your framework's output directory run: - base: static + base: alpine/static ``` -The key is pointing `deployFiles` to wherever your framework outputs its built files (`dist/`, `build/`, `.output/public/`, etc.). +The key is pointing `deployFiles` to wherever your framework outputs its built files (`dist/`, `build/`, `.output/public/`, etc.). The trailing `/~` deploys the folder's contents to the document root. This configuration: 1. Uses Node.js for building the application @@ -5548,6 +5700,20 @@ For framework-specific examples, check out our [recipe collection](https://githu ## Advanced Topics +### Custom Nginx configuration + +The Static service also accepts your own Nginx configuration through `run.siteConfigPath`, the same attribute the [Nginx service](/nginx/how-to/customize-web-server) uses. The configuration is chosen with this precedence: + +1. `run.routing` is set: the configuration is generated from it and `siteConfigPath` is ignored +2. Only `run.siteConfigPath` is set: your file is used as the complete `server` configuration +3. Neither is set: the generated default configuration is used + +A `.tmpl` file is rendered as a Go template with `{{.DocumentRoot}}` (the value of `run.documentRoot`, `/var/www` when unset) and `{{.Environment.NAME}}` for environment variables. Any other extension is copied verbatim. See the [Nginx service guide](/nginx/how-to/customize-web-server#customize-nginx-configuration) for the requirements a custom configuration must meet. + +:::tip +If you find yourself writing a custom configuration, consider switching to the [Nginx service](/nginx/overview). It is the same Nginx with `documentRoot` and `siteConfigPath` as first-class options, and it is what the rest of the documentation assumes for hand-written configurations. +::: + ### Switching to Full Nginx If you need more control over your Nginx configuration: @@ -5558,6 +5724,10 @@ If you need more control over your Nginx configuration: 4. Copy the generated Nginx configuration 5. Use this configuration as a starting point for a full Nginx service +The same file is available inside any running container of the service at `/etc/nginx/sites-enabled/default.site`, for example over [SSH](/references/networking/ssh). + +To migrate, change `run.base` to `alpine/nginx@latest` (or `ubuntu/nginx@latest`), replace `run.routing` with `run.siteConfigPath` pointing at the copied configuration (with the `root` directive adjusted or replaced by `{{.DocumentRoot}}`), and remove `routing`, since the Nginx service ignores it. Prerender.io support is part of the generated configuration and will be carried over with it. + :::tip This allows you to move to a more customizable setup while maintaining your existing routing logic. ::: @@ -5580,7 +5750,7 @@ run: to: https://main-site.com/campaign status: 302 - # Legacy domains and subdomains + # Legacy subdomains - from: https://*.legacy-domain.com/* to: https://main-site.com status: 302 @@ -5590,7 +5760,7 @@ run: ### Development Setup -Configuration for local development with CORS and API proxying: +Configuration for a development environment with CORS and an API on another domain: ```yaml title="zerops.yaml" run: @@ -5598,12 +5768,18 @@ run: # CORS with proper quoting cors: "'*' always" redirects: - # API requests + # Send browsers calling /api/... to the API domain - from: /api/* - to: https://api.your-domain.com - status: 302 + to: https://api.your-domain.com/ + status: 307 + preservePath: true + preserveQuery: true ``` +:::note +This is a browser redirect, not a reverse proxy. The Static service cannot proxy requests. To serve an API under the same domain as the frontend, use [domain access routing](/references/networking/public-access#http-routing-setup) or the [Nginx service](/nginx/overview) with a `proxy_pass` configuration. +::: + ### Production Setup with Security Security-enhanced configuration for production environments: @@ -5612,7 +5788,7 @@ Security-enhanced configuration for production environments: run: routing: headers: - # Custom headers for default location + # Custom headers for the whole site - for: "/*" values: X-Frame-Options: "'DENY' always" @@ -5621,6 +5797,7 @@ run: Content-Security-Policy: '"default-src ''self''" always' ``` + ---------------------------------------- # Shared Storage > Overview @@ -9014,14 +9191,68 @@ zsc crontab [command] --- -### execOnce +### env-replace + +Renders template files with environment variables. Placeholders in the form `{{.VARIABLE_NAME}}` are replaced with the value of the environment variable of that name and the result is written to the target. Source files are never modified. + +```sh +zsc env-replace [flags] +``` + +`envReplace` is accepted as an alias. This is a different mechanism than the [`envReplace`](/zerops-yaml/specification#envreplace-) key in `zerops.yaml`, which rewrites `%%VARIABLE%%`-style placeholders in the deployed files in place. + +#### Required parameters +- ``: Template file or directory to read +- ``: File or directory to write the rendered result to + +Both paths can be absolute (starting with `/`) or relative to the current working directory. + +- **Source directory**: traversed recursively, every file is rendered to the same relative path under the target directory. Missing target directories are created. +- **Source file**: rendered to the target path. When the target is an existing directory, the file is written into it under its original name. + +The target must not be the source itself or a path inside the source directory. + +#### Available flags +- `-s, --silent`: Disables output to StdOut +- `-h, --help`: Help for the env-replace command + +#### Template syntax +Templates use Go `text/template` syntax. Use `{{if .OPTIONAL}}...{{end}}` to guard a variable that may be unset. A file name ending with a permission suffix such as `script.sh.0755` is written without the suffix and with that file mode. + +#### Examples +```sh +# Render every template under /template/etc into /etc +zsc env-replace /template/etc /etc + +# Render a single file to an explicit target path +zsc env-replace /template/etc/example.conf /etc/example.conf + +# Relative paths are resolved from the current working directory +zsc env-replace templates/nginx.conf /etc/nginx/nginx.conf +``` + +#### Example usage in `zerops.yaml` +```yaml +zerops: + - setup: app + run: + initCommands: + - zsc env-replace ./templates/app.conf /etc/app/app.conf + start: /usr/bin/app --config /etc/app/app.conf +``` + +--- + +### exec-once Execute a command exactly once across all containers in a service, preventing duplicate execution in high-availability setups. ```sh -zsc execOnce [flags] -- [args...] +zsc exec-once [flags] -- [args...] ``` +`execOnce` is accepted as an alias. + #### Required parameters * ``: A unique identifier for the execution * `--`: Standard separator indicating the end of options and beginning of the command @@ -9030,7 +9261,7 @@ zsc execOnce [flags] -- [args...] #### Available flags - `-r, --retryUntilSuccessful`: Retry command until it succeeds - `-v, --verbose`: Verbose output -- `-h, --help`: Help for the execOnce command +- `-h, --help`: Help for the exec-once command #### Behavior - **On success**: All containers proceed with their tasks @@ -9040,10 +9271,10 @@ zsc execOnce [flags] -- [args...] #### Examples ```sh # Execute a command once for the entire service stack -zsc execOnce someStaticKey -- /var/www/myBinary some initial command --flag="value" --flag2="value2" +zsc exec-once someStaticKey -- /var/www/myBinary some initial command --flag="value" --flag2="value2" # Run migrations for each new app version deployed to Zerops -zsc execOnce ${ZEROPS_appVersionId} -- php /var/bin/console migrations:continue +zsc exec-once ${ZEROPS_appVersionId} -- php /var/bin/console migrations:continue ``` :::info @@ -9320,39 +9551,56 @@ The container will scale down automatically if resources are not utilized, or if --- -### setSecretEnv +### set-env -Securely update environment variables containing sensitive information. +Set or update an environment variable of the current service. ```sh -zsc setSecretEnv +zsc set-env [flags] ``` +`setEnv` is accepted as an alias. + #### Arguments - ``: The name of the environment variable to set - ``: The new value for the variable, or `-` to read from stdin #### Available flags -- `-h, --help`: Help for the setSecretEnv command +- `--sensitive`: Store the variable as a secret. When omitted, an existing variable keeps its current secret setting and a new one is created as a plain variable +- `-h, --help`: Help for the set-env command #### Examples ```sh -# Set a secret environment variable directly -zsc setSecretEnv SECRET_KEY "new_value" +# Set an environment variable directly +zsc set-env KEY "new_value" -# Set a secret environment variable from stdin (useful for multi-line values or piping) -echo "new_value" | zsc setSecretEnv SECRET_KEY - +# Set an environment variable from stdin (useful for multi-line values or piping) +echo "new_value" | zsc set-env KEY - # Set a secret API key from a file -cat api_key.txt | zsc setSecretEnv API_KEY - +cat api_key.txt | zsc set-env API_KEY - --sensitive ``` :::info -Secret environment variables are encrypted at rest and securely distributed to your containers. Use this command for storing sensitive configuration like API keys, tokens, and passwords. +Secret environment variables are encrypted at rest and securely distributed to your containers. Use `--sensitive` for sensitive configuration like API keys, tokens, and passwords. ::: --- +### set-secret-env + +:::caution +Deprecated, use [`set-env --sensitive`](#set-env) instead. The command keeps working for existing scripts and recipes, `setSecretEnv` is accepted as an alias. +::: + +```sh +zsc set-secret-env +``` + +Behaves like `set-env` with `--sensitive` always on. + +--- + ### shared-storage Mounts a [SeaweedFS](/seaweedfs/overview) service at `/mnt/` with `weed mount`. The command dates from the deprecated Shared Storage and keeps its name, it is a shortcut for running `weed mount` yourself: it creates the directory, hands it to the `zerops` user, connects to the filer of the first storage container (`node-stable-1.db..zerops:8888`) and passes a RAM-lean tuning (`-volumeServerAccess=direct -cacheCapacityMB=0 -concurrentWriters=1 -chunkSizeLimitMB=1`), see [Mounting from a runtime service](/seaweedfs/overview#mounting-from-a-runtime-service). `mount` runs in the foreground, so use it as a [start command](/zerops-yaml/specification#startcommands-). With `--background` it detaches the mount process and returns once the mount is ready, which is the form for [init commands](/seaweedfs/overview#mounting-in-init-commands). Both need `sudo`. @@ -9973,22 +10221,6 @@ services: minContainers: 2 # Maximum number of containers maxContainers: 6 - # Full nginx config - nginxConfig: |- - server { - listen 80 default_server; - listen [::]:80 default_server; - - server_name _; - root /var/www; - - location / { - try_files $uri $uri/ /index.html; - } - - access_log syslog:server=unix:/dev/log,facility=local1 default_short; - error_log syslog:server=unix:/dev/log,facility=local1; - } # Zerops.yaml configuration zeropsSetup: backendapi zeropsYaml: @@ -10103,7 +10335,6 @@ The Service Configuration section is divided into multiple subsections for bette - [**Service Basic Configuration**](#service-basic-configuration) - Core parameters like hostname, type, mode, and environment variables - [**Service Vertical Autoscaling**](#service-vertical-autoscaling) - CPU, RAM, and disk scaling settings - [**Service Horizontal Autoscaling**](#service-horizontal-autoscaling) - Container count scaling settings -- [**Service Nginx Configuration**](#service-nginx-configuration) - Custom web server settings - [**Service zerops.yaml Configuration**](#service-zeropsyaml-configuration) - Build and run configurations #### Complete Service Configuration Example @@ -10163,22 +10394,6 @@ services: minContainers: 2 # Minimum number of containers (default: 1, max: 10) maxContainers: 6 # Maximum number of containers (max: 10) - # Nginx configuration - nginxConfig: |- # Custom nginx configuration - server { - listen 80 default_server; - listen [::]:80 default_server; - server_name _; - root /var/www/public; - - location / { - try_files $uri $uri/ /index.html; - } - - access_log syslog:server=unix:/dev/log,facility=local1 default_short; - error_log syslog:server=unix:/dev/log,facility=local1; - } - # Zerops.yaml configuration zeropsSetup: backendapi # Service setup name from zeropsYaml or repo zeropsYaml: # Full zerops.yaml configuration @@ -10545,52 +10760,6 @@ The `minContainers` and `maxContainers` parameters allow you to define the minim The `mount:` field of the deprecated [Shared Storage](/shared-storage/overview) is no longer accepted, an import containing it fails with `yamlMountDeprecated`. Mount a [SeaweedFS](/seaweedfs/overview) service from the runtime's [`startCommands`](/zerops-yaml/specification#startcommands-) in zerops.yaml, or mount a [Local Storage](/local-storage/overview) volume through the [`volume` field](/zerops-yaml/specification#volume-). Existing setups can be moved with the [SeaweedFS migration guide](/seaweedfs/how-to/migrate-from-shared-storage). ::: -### Service Nginx Configuration - -The nginx configuration defines the nginx settings for the service. - - - - - - - - - - - - - - - - -
FieldTypeDescription
nginxConfigstring (multiline)Insert full nginx config.
- -```yaml -#yamlPreprocessor=on -services: - - hostname: app - type: php-nginx@8.4 - enableSubdomainAccess: true - nginxConfig: |- - server { - listen 80 default_server; - listen [::]:80 default_server; - - server_name _; - root /var/www; - - location / { - try_files $uri $uri/ /index.html; - } - - access_log syslog:server=unix:/dev/log,facility=local1 default_short; - error_log syslog:server=unix:/dev/log,facility=local1; - } -``` - -The `nginxConfig: |-` parameter allows you to specify a custom nginx configuration for the service. - ### Service zerops.yaml Configuration The `zeropsSetup` and `zeropsYaml` parameters provide flexibility in how you define and use your service configurations. Both parameters are optional and work together in the following ways: @@ -22449,7 +22618,11 @@ Read more about how the [readiness check works](/nodejs/how-to/deploy-process#re # Nginx > Overview -The Nginx static service contains the [Nginx ↗](https://nginx.org/) web server optimized for your static content. +The Nginx static service contains the [Nginx ↗](https://nginx.org/) web server optimized for your static content. You get a plain Nginx with a sensible default configuration and full control over it through your own configuration file. + +:::tip Static or Nginx? +Zerops also offers the [Static service](/static/overview), which generates the Nginx configuration for you from a declarative `run.routing` section (redirects, headers, CORS) and has Prerender.io built in. Choose the Nginx service when you need to write Nginx configuration yourself, for example for `proxy_pass`, caching, or additional ports. The `run.routing` section is ignored by the Nginx service, and `run.documentRoot` is ignored by the Static service. See the [comparison](/static/overview#static-or-nginx-service). +::: ## How to start @@ -22460,7 +22633,7 @@ The Nginx static service contains the [Nginx ↗](https://nginx.org/) web server - [Create Nginx static service](/nginx/how-to/create) — Start with creating a Nginx static service using GUI or zCLI. - [zerops.yaml](/nginx/how-to/build-pipeline#add-zeropsyaml-to-your-repository) — See a full example of zerops.yaml file to create your own app. - [Scaling configuration](/nginx/how-to/scaling) — Set up scaling of your Nginx application so that it runs smoothly while using only necessary resources. -- [SEO Optimization](/nginx/how-to/env-variables#prerenderio-support) — Built-in prerender.io support for better SEO. +- [Custom Nginx configuration](/nginx/how-to/customize-web-server) — Bring your own Nginx configuration with siteConfigPath. {" "} @@ -22489,9 +22662,17 @@ Have you build something that others might find useful? Don't hesitate to share # Nginx > Faq + **Question: What is the difference between the Nginx service and the Static service?** + +Both serve static files with Nginx. The [Static service](/static/overview) generates the Nginx configuration from a declarative `run.routing` section (redirects, headers, CORS) and has Prerender.io built in. The Nginx service gives you the default configuration or your own file via `run.siteConfigPath`, which is the way to go for `proxy_pass`, caching or extra ports. See the [comparison](/static/overview#static-or-nginx-service). + + **Question: Why is my run.routing section ignored?** + +`run.routing` is only read by the Static service. The Nginx service ignores it without an error. Either switch `run.base` to `alpine/static`, or express the redirects and headers in a custom configuration set via [`siteConfigPath`](/nginx/how-to/customize-web-server). + **Question: How do I enable SEO optimization with prerender.io?** -Zerops provides built-in prerender.io support. Simply set the `PRERENDER_TOKEN` environment variable with your prerender.io service token. See our [prerender.io documentation](/nginx/how-to/env-variables#prerenderio-support) for details. +Prerender.io support is built into the [Static service](/static/overview#seo-with-prerender), not into the Nginx service. On the Nginx service you can add the prerender rules to your own configuration set via [`siteConfigPath`](/nginx/how-to/customize-web-server), or switch to the Static service. ---------------------------------------- @@ -22529,48 +22710,14 @@ Zerops provides built-in prerender.io support. Simply set the `PRERENDER_TOKEN` # Nginx > How To > Env Variables -## Prerender.io Support +## Environment variables in the Nginx configuration -Zerops provides built-in prerender.io support for SEO optimization. Configure it using these environment variables: +Nginx itself does not read environment variables. To use them in your configuration, deploy a `.tmpl` file via [`siteConfigPath`](/nginx/how-to/customize-web-server#customize-nginx-configuration) and reference variables as `{{.Environment.NAME}}`. Zerops renders the template every time a container starts. - - - - - - - - - - - - - - - - - - - - - - - -
VariableRequiredDescriptionDefault
PRERENDER_TOKENYesYour prerender.io service token-
PRERENDER_HOSTNoPrerender service hostservice.prerender.io
- -:::tip -Set `PRERENDER_TOKEN` as a secret environment variable in Zerops GUI for security. +:::note +Prerender.io support (`PRERENDER_TOKEN`, `PRERENDER_HOST`) is a feature of the [Static service](/static/overview#seo-with-prerender). The Nginx service does not act on these variables. ::: -Example in zerops.yaml: -```yaml -zerops: - - setup: app - run: - envVariables: - PRERENDER_HOST: "custom.prerender.host" -``` - ---------------------------------------- # Nginx > How To > Deploy Process @@ -22594,6 +22741,11 @@ server { server_name _; root {{.DocumentRoot}}; + location ^~ /.git { + deny all; + return 404; + } + location / { try_files $uri $uri/ /index.html; } @@ -22603,15 +22755,23 @@ server { } ``` -The configuration contains 2 variables: +The configuration contains one variable: - **`{{.DocumentRoot}}`** is replaced by the [`run.documentRoot`](/nginx/how-to/build-pipeline#documentroot) attribute from the `zerops.yaml`. If the attribute is not specified, the default value `/var/www` is used. +With this configuration a request is served from the exact path, then from `index.html` inside a directory (a request for `/docs` is redirected to `/docs/` first), and falls back to `/index.html` for everything else, which makes client-side routing of Single Page Applications work. Extensionless URLs such as `/about` for `about.html` are **not** resolved, and anything under `/.git` returns 404. Requests for a nonexistent path return `/index.html` with status 200, not 404. + +The surrounding `nginx.conf` enables gzip for text-based content types, hides the Nginx version and allows request bodies up to 10 GB. + +:::note +Redirects, custom headers and CORS have no declarative configuration on the Nginx service. Write them in your own configuration as described below. The `run.routing` section of `zerops.yaml` belongs to the [Static service](/static/overview#routing--configuration) and is ignored here. +::: + ## Customize Nginx configuration Follow these steps to customize the Nginx configuration in Nginx static service: -1. Create a **.tmpl** file with the Nginx configuration in your repository. +1. Create a **.tmpl** file with the Nginx configuration in your repository. The file replaces the whole default `server` block shown above, so start from a copy of it. 2. Optionally use following variables: @@ -22625,8 +22785,16 @@ root {{.DocumentRoot}}; - **`{{.Environment.ENV_NAME}}`** is replaced by the [env variable](/nginx/how-to/env-variables) value. The env variable must be either defined in [run.envVariables](/nginx/how-to/build-pipeline#envvariables) in `zerops.yaml` or set as a [secret](/nginx/how-to/env-variables#set-secret-env-variables-in-zerops-gui) or [generated](/nginx/how-to/env-variables#generated-env-variables) env variable in Zerops GUI. +Example: + +``` +location /api/ { + proxy_pass http://{{.Environment.API_HOSTNAME}}:3000/; +} +``` + :::caution -Use the **.tmpl** file extension to make Zerops interpret the file as a template. Zerops will replace the supported variables listed above. +Use the **.tmpl** file extension to make Zerops interpret the file as a template. Zerops will replace the supported variables listed above. A file with any other extension is copied to Nginx verbatim, without variable replacement. ::: 3. Check that your Nginx configuration is consistent with Zerops requirements: @@ -22634,6 +22802,7 @@ Use the **.tmpl** file extension to make Zerops interpret the file as a template - Do not use IP addresses in the `listen` directive - If you use other ports than `:80` in the `listen` directive, add them to the [`run.ports`](/nginx/how-to/build-pipeline#ports) in your `zerops.yaml` as well. - Do not use the port **:443**. All the incoming `https://` traffic is terminated on the Zerops internal balancer where the SSL certificate is installed and the request is forwarded to your Nginx static service as a **http://** on the port **:80**. +- Keep the `access_log` and `error_log` directives from the default configuration so that the logs show up in Zerops. 4. Add the [`siteConfigPath`](/nginx/how-to/build-pipeline#siteconfigpath) to the run section of your `zerops.yaml` @@ -22646,40 +22815,76 @@ zerops: # REQUIRED. Set the base technology for the build environment: base: nodejs@latest + buildCommands: + - npm i + - npm run build + # REQUIRED. Select which files / folders to deploy after # the build has successfully finished deployFiles: - - vendor - - public + - dist + - site_config.tmpl # ==== how to run your application ==== run: - documentRoot: public + base: alpine/nginx@latest - # OPTIONAL. Sets the custom Nginx or Apache configuration. The file must be deployed in the runtime container. Enter the path to the file relative to the /var/www folder + # OPTIONAL. Folder served by Nginx, relative to /var/www + documentRoot: dist + + # OPTIONAL. Sets the custom Nginx configuration. The file must be deployed in the runtime container. Enter the path to the file relative to the /var/www folder siteConfigPath: site_config.tmpl ``` 5. Ensure that the [`build.deployFiles`](/nginx/how-to/build-pipeline#deployfiles) contains the folder with the `siteConfigPath` or add the path to the Nginx config file to the `deployFiles` list. Zerops will deploy the file to the runtime container(s). -6. [Trigger](/nginx/how-to/trigger-pipeline) the build & deploy pipeline. +:::tip +Everything under the document root is publicly served, including a configuration file deployed there. Keep the configuration outside the `documentRoot` folder (as in the example above, where `documentRoot` is `dist` and the template sits next to it in `/var/www`). +::: -## SEO & Prerender Support +6. [Trigger](/nginx/how-to/trigger-pipeline) the build & deploy pipeline. The configuration is applied every time a container starts, and the deploy fails if the file is missing. -Single Page Applications and JavaScript-heavy sites render content client-side, which most crawlers can't process—they see an empty page instead of your content. This affects traditional search engines, social media platforms, and AI tools like ChatGPT, Perplexity, and Claude. +## Common customizations -### Built-in Prerender.io Integration +Serve extensionless `.html` pages and keep the SPA fallback: -The default Nginx configuration includes automatic [Prerender.io](https://prerender.io) support. When enabled, it detects crawler requests (including AI crawlers) and serves them pre-rendered HTML while your users get the full interactive experience. +``` +location / { + try_files $uri $uri.html $uri/index.html /index.html =404; +} +``` -### Setup +Redirect an old path permanently: -To enable prerender support: +``` +location = /old-page { + return 301 /new-page; +} +``` + +Add headers to every response: + +``` +add_header X-Frame-Options 'DENY' always; +add_header X-Content-Type-Options 'nosniff' always; +``` -1. Set the `PRERENDER_TOKEN` environment variable with your Prerender.io token (see [environment variables](/nginx/how-to/env-variables#prerenderio-support)) -2. Optionally set `PRERENDER_HOST` if using a custom prerender server +Proxy a path to another service of the project (the hostname is the other service's name): + +``` +location /api/ { + proxy_pass http://api:3000/; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +} +``` + +## SEO & Prerender Support + +Prerender.io support is not part of the default Nginx service configuration. It is built into the [Static service](/static/overview#seo-with-prerender), which enables it as soon as `PRERENDER_TOKEN` is set. + +If you need prerendering together with a hand-written Nginx configuration, either start from the configuration generated by a Static service (open the Static service in the GUI and choose **Need to switch to full Nginx service?**, or copy `/etc/nginx/sites-enabled/default.site` from one of its containers) or add the [Prerender.io Nginx snippet](https://docs.prerender.io/docs/nginx-2) to your own template. -The Nginx configuration will automatically handle the rest—no additional configuration needed. ---------------------------------------- @@ -22700,10 +22905,10 @@ You should not include your application code in the custom runtime image, as you The default Nginx Static runtime environment contains: -- {data.alpine.default} +- {data.alpine.default} with `alpine/nginx@latest`, or Ubuntu with `ubuntu/nginx@latest` - Selected version of Nginx when the runtime service was created - [zCLI](/references/cli) -- Git +- Git, wget and curl ### When You Need a Custom Runtime Image @@ -22713,6 +22918,8 @@ Here are Nginx Static-specific examples of configuring custom runtime images in ### Basic Nginx Static Setup +Use `sudo apk add` on Alpine and `sudo apt-get install -y` on Ubuntu. + For complete configuration details, see the [runtime prepare phase configuration guide](/features/pipeline#configuration). ## Process and Caching @@ -22741,6 +22948,10 @@ If your `prepareCommands` fail, check the [prepare runtime log](/nginx/how-to/lo The Nginx static service contains the Nginx web server optimized for your static content. Nginx static service is highly scalable and customisable to suit both development and production. +:::tip +If you only need redirects, headers and CORS on top of your static files, the [Static service](/static/overview) configures them declaratively without any Nginx configuration. See [Static or Nginx?](/static/overview#static-or-nginx-service). +::: + ## Create Nginx static service using Zerops GUI First, set up a project in Zerops GUI. Then go to the project dashboard page and choose **Add new service** in the left menu in the **Services** block. Then add a new Nginx static service: @@ -22801,11 +23012,11 @@ project: services: - # service name hostname: app - # service type and version number in nginx@{version} format - type: nginx@latest + # service type, OS and version number in {os}/nginx@{version} format + type: ubuntu/nginx@latest # defines the minimum number of containers for horizontal autoscaling minContainers: 1 - # defines the maximum number of containers for horizontal autoscaling. Max value = 6. + # defines the maximum number of containers for horizontal autoscaling. maxContainers: 6 # optional: create env variables envSecrets: @@ -22813,7 +23024,11 @@ services: S3_ACCESS_SECRET: 'ogFthuiLYki8XoL73opSCQ' ``` -The yaml file describes your future project infrastructure. The project will contain one Nginx version 8.1 service with default [auto scaling](/nginx/how-to/scaling) configuration. Hostname will be set to "app", the internal port(s) the service listens on will be defined later in the [zerops.yaml](/nginx/how-to/build-pipeline#ports). Following secret env variables will be configured: +:::note Operating system +Always include the OS in the type: `alpine/nginx@latest` or `ubuntu/nginx@latest`, and use the same value as `run.base` in your `zerops.yaml`. A bare `nginx@latest` is resolved to Ubuntu here but to Alpine in `zerops.yaml`, so the first deploy would silently switch the service to another OS. +::: + +The yaml file describes your future project infrastructure. The project will contain one Nginx version 1.22 service with default [auto scaling](/nginx/how-to/scaling) configuration. Hostname will be set to "app", the internal port(s) the service listens on will be defined later in the [zerops.yaml](/nginx/how-to/build-pipeline#ports). Following secret env variables will be configured: ```env S3_ACCESS_KEY_ID="P8cX1vVVb" @@ -22839,8 +23054,8 @@ project: services: - # service name hostname: app - # service type and version number in nginx@{version} format - type: nginx@latest + # service type, OS and version number in {os}/nginx@{version} format + type: ubuntu/nginx@latest # optional: vertical auto scaling customization verticalAutoscaling: cpuMode: DEDICATED @@ -22853,9 +23068,9 @@ services: startCpuCoreCount: 3 minFreeRamGB: 0.5 minFreeRamPercent: 20 - # defines the minimum number of containers for horizontal autoscaling. Max value = 6. + # defines the minimum number of containers for horizontal autoscaling. minContainers: 2 - # defines the maximum number of containers for horizontal autoscaling. Max value = 6. + # defines the maximum number of containers for horizontal autoscaling. maxContainers: 4 # optional: create secret env variables envSecrets: @@ -23052,11 +23267,11 @@ project: services: - # service name hostname: app - # service type and version number in nginx@{version} format - type: nginx@latest + # service type, OS and version number in {os}/nginx@{version} format + type: ubuntu/nginx@latest # defines the minimum number of containers for horizontal autoscaling minContainers: 1 - # defines the maximum number of containers for horizontal autoscaling. Max value = 6. + # defines the maximum number of containers for horizontal autoscaling. maxContainers: 6 # optional: create env variables envSecrets: @@ -23103,6 +23318,10 @@ Maximum size of the import.yaml file is 100 kB. Zerops provides a customizable build and runtime environment for your static content. +:::note Nginx service vs Static service +This page covers the Nginx service (`run.base: alpine/nginx@latest` or `ubuntu/nginx@latest`). The [Static service](/static/overview) (`run.base: alpine/static`) uses the same build and deploy pipeline but is configured through `run.routing` instead of `documentRoot` and `siteConfigPath`. The `run.routing` section is ignored by the Nginx service. See [Static or Nginx?](/static/overview#static-or-nginx-service). +::: + :::tip Two Deployment Approaches You can use the Nginx service in two ways: - **Runtime only**: Deploy pre-built static files directly (no build phase needed) @@ -23129,9 +23348,6 @@ zerops: # REQUIRED (if using build). Set the base technology for the build environment: base: nodejs@latest - # OPTIONAL. Set the operating system for the build environment. - # os: ubuntu - # OPTIONAL. Customize the build environment by installing additional packages # or tools to the base build environment. # prepareCommands: @@ -23156,8 +23372,8 @@ zerops: # ==== how to run your application ==== run: - # OPTIONAL. Sets the base technology for the runtime environment: - base: nginx@latest + # OPTIONAL. Sets the base technology and OS for the runtime environment: + base: alpine/nginx@latest # OPTIONAL. Customize the runtime Nginx environment by installing additional # dependencies to the base Nginx runtime environment. @@ -23233,14 +23449,16 @@ zerops: # ==== how to run your application ==== run: - # OPTIONAL. Sets the base technology for the runtime environment: - base: nginx@latest + # OPTIONAL. Sets the base technology and OS for the runtime environment: + base: alpine/nginx@latest ... ``` +Always use the OS-prefixed form (`alpine/nginx@latest`, `ubuntu/nginx@latest`). A bare `nginx@latest` is resolved differently in different places (Alpine in `zerops.yaml`, Ubuntu in an import YAML), so a deploy can silently switch the service to another OS. +

The base runtime environment contains {data.alpine.default}, the - selected major version of Nginx, [Zerops command line tool](/references/cli) and `composer`, `git` and `wget`. + selected major version of Nginx, [Zerops command line tool](/references/cli), `git`, `wget` and `curl`.

:::info @@ -23263,7 +23481,7 @@ zerops: run: # OPTIONAL. Sets the base technology for the runtime environment: base: - - nginx@latest + - alpine/nginx@latest prepareCommands: - zsc add go@latest ... @@ -23275,14 +23493,7 @@ To customize your build environment use the `prepareCommands` attribute. ### os -_OPTIONAL._ Sets the operating system for the runtime environment. - -Following options are available: - -- `alpine` -- `ubuntu` - -Default value is `alpine`. +_DEPRECATED._ The operating system is part of the `base` value: `alpine/nginx@latest` or `ubuntu/nginx@latest`. Do not set `os` separately. We are currently using following os version: @@ -23344,7 +23555,7 @@ _OPTIONAL._ Customizes the Nginx runtime environment by installing additional de

The base Nginx environment contains {data.alpine.default}, the selected - major version of Nginx, [Zerops command line tool](/references/cli) and `composer`, `git` and `wget`. To install + major version of Nginx, [Zerops command line tool](/references/cli), `git`, `wget` and `curl`. To install additional packages or tools add one or more prepare commands:

@@ -23399,7 +23610,7 @@ You can configure your prepare commands to be run in a single shell instance or

The prepare runtime container contains {data.alpine.default}, the - selected major version of Nginx, [Zerops command line tool](/references/cli) and `composer`, `git` and `wget`. + selected major version of Nginx, [Zerops command line tool](/references/cli), `git`, `wget` and `curl`.

The prepare runtime container does not contain your application code nor the built application. If you need to copy some folders or files from the build container to the runtime container (e.g. a configuration file) use the `addToRunPrepare` attribute in the build section of your chosen technology. @@ -23497,12 +23708,30 @@ zerops: _OPTIONAL._ Sets the custom Nginx configuration. :::info -If you don't set your custom configuratiin Zerops applies the [default](/nginx/how-to/customize-web-server#default-nginx-configuration) configuration. +If you don't set your custom configuration Zerops applies the [default](/nginx/how-to/customize-web-server#default-nginx-configuration) configuration. ::: -The file must be deployed in the runtime container. Enter the path to the file relative to the `/var/www` folder. +The file must be deployed in the runtime container. Enter the path to the file relative to the `/var/www` folder. A file with the `.tmpl` extension is rendered as a template (`{{.DocumentRoot}}`, `{{.Environment.NAME}}`), any other file is used verbatim. Read more about the [web server customization](/nginx/how-to/customize-web-server). +```yaml +zerops: + # hostname of your service + - setup: app + # ==== how to build your application ==== + build: + ... + deployFiles: + - dist + - site_config.tmpl + + # ==== how to run your application ==== + run: + documentRoot: dist + # OPTIONAL. Path to your Nginx configuration, relative to /var/www. + siteConfigPath: site_config.tmpl +``` + ### envVariables _OPTIONAL._ Defines the environment variables for the runtime environment. @@ -27872,7 +28101,7 @@ run: - litestream restore -if-replica-exists -if-db-not-exists $DB_NAME ``` -Each entry: `command` (**required** — the only field the schema requires), `name` (optional, distinguishes processes in logs), `workingDir` (optional), `initCommands` (optional, per-process init). **DO NOT** use both `start` and `startCommands`. +Each entry: `command` (**required** — the only field the schema requires), `name` (optional, distinguishes processes in logs), `workingDir` (optional), `user` (optional, system user the command and its `initCommands` run under, default `zerops`, the user has to exist in the container), `initCommands` (optional, per-process init). **DO NOT** use both `start` and `startCommands`. --- @@ -27920,12 +28149,12 @@ run: values: { X-Frame-Options: "'DENY'" } ``` -- **`cors`**: Sets Access-Control-Allow-Origin. `"*"` auto-converted to `'*'` -- **`redirects[]`**: `from` (wildcards `*`), `to`, `status`, `preservePath`, `preserveQuery` -- **`headers[]`**: `for` (path pattern), `values` (header key-value pairs) -- **`root`**: Custom root directory +- **`cors`**: Sets Access-Control-Allow-Origin, -Allow-Methods, -Allow-Headers and -Expose-Headers to the same value. `"*"` auto-converted to `'*'` +- **`redirects[]`**: `from` (exact path, or prefix with trailing `*`, or absolute URL matched on the domain), `to`, `status` (omit for a masked redirect, `301`/`302` only for absolute `from`), `preservePath`, `preserveQuery` (both only with a `status`) +- **`headers[]`**: `for` (same matching as `from`, `"/*"` for everything, `"/"` is the homepage only), `values` (header key-value pairs, quotes included: `"'DENY'"`). Only the best-matching rule applies, rules are not merged. +- **`root`**: Folder served, relative to `/var/www`. The Static service ignores `run.documentRoot`, this is its replacement. -**DO NOT** use on non-static services -- silently ignored. +**DO NOT** use on non-static services -- silently ignored. Conversely `documentRoot` is ignored on Static, and `siteConfigPath` is ignored on Static whenever `routing` is present. --- @@ -27963,7 +28192,7 @@ Available runtimes and versions are listed in **Service Stacks (live)** -- injec - The OS is part of the base value — `ubuntu/nodejs@22` or `alpine/nodejs@22` — with no separate `os:` field; use the same prefix in `build.base` and `run.base` (a bare `nodejs@22` resolves to Alpine, and `run.base` rewrites the service OS on deploy) - PHP: build `/php@X`, run `/php-nginx@X` or `/php-apache@X` (different bases) - Deno: ships only as `ubuntu/deno@X` (no Alpine image). Gleam runs on both Alpine and Ubuntu. -- Static sites: build `alpine/nodejs@latest`, run `static` +- Static sites: build `alpine/nodejs@latest`, run `alpine/static` (the bare `static` shorthand means the same) - `@latest` = newest stable version --- diff --git a/apps/docs/static/llms-small.txt b/apps/docs/static/llms-small.txt index a32f0ba5..17764585 100644 --- a/apps/docs/static/llms-small.txt +++ b/apps/docs/static/llms-small.txt @@ -268,10 +268,7 @@ build: ### os *[Optional]* -Sets the operating system for the build environment. Options: - -- `alpine` (default) -- `ubuntu` (default for ubuntu service) +Deprecated. The operating system is part of the `base` value, for example `ubuntu/nodejs@22` or `alpine/nodejs@22`. Use the OS-prefixed form in both `build.base` and `run.base` instead of setting `os`. A bare `nodejs@22` defaults to Alpine. Current versions: @@ -280,7 +277,7 @@ Current versions: ```yaml build: - os: ubuntu + base: ubuntu/nodejs@22 ``` ### prepareCommands *[Optional]* @@ -482,7 +479,7 @@ run: ### os *[Optional]* -Sets the operating system for the runtime environment. Options and versions are the same as for the build environment. +Deprecated, same as for the build environment: put the OS into `run.base` (`ubuntu/nodejs@22`) instead. ### ports *[Optional]* @@ -565,15 +562,45 @@ run: - litestream restore -if-replica-exists -if-db-not-exists -config=litestream.yaml $DB_NAME ``` +Each entry supports: + +- `command` (required) - the command to run +- `name` (optional) - distinguishes the process in logs +- `workingDir` (optional, default `/var/www`) - the directory the command and its `initCommands` run in +- `user` (optional, default `zerops`) - the system user the command and its `initCommands` run under. The user has to exist in the runtime container, create it in `prepareCommands`. +- `initCommands` (optional) - commands run before this process starts, each time a container starts or restarts + +```yaml +run: + prepareCommands: + - sudo adduser --system --group --home /home/git git + startCommands: + - command: gitea web + name: gitea + user: git +``` + See [start-commands-example](https://github.com/zeropsio/start-commands-example) ### documentRoot *[Optional]* -Customizes the root folder for publicly accessible web server content (available only for webserver runtimes). +Customizes the root folder for publicly accessible web server content. The path is relative to `/var/www`. Available for the [Nginx](/nginx/how-to/build-pipeline#documentroot) and [PHP](/php/how-to/build-pipeline#documentroot) services. The [Static service](/static/overview#document-root) ignores it, use `routing.root` there. + +```yaml +run: + base: alpine/nginx@latest + documentRoot: dist +``` ### siteConfigPath *[Optional]* -Sets the custom webserver configuration (available only for webserver runtimes). +Sets the custom webserver configuration for the [Nginx](/nginx/how-to/customize-web-server), [PHP](/php/how-to/customize-web-server) and [Static](/static/overview#custom-nginx-configuration) services. The path is relative to `/var/www` and the file must be part of the deployed files. A `.tmpl` file is rendered as a template with `{{.DocumentRoot}}` and `{{.Environment.NAME}}`, any other file is used verbatim. On the Static service `routing` takes precedence: when both are set, `siteConfigPath` is ignored. + +```yaml +run: + base: alpine/nginx@latest + siteConfigPath: site_config.tmpl +``` ### envVariables *[Optional]* @@ -619,6 +646,10 @@ Files or directories to process for variable replacement. Directory targets only process files directly in the specified directory, not subdirectories for performance reasons. To process files in subdirectories, specify each subdirectory explicitly in the target array. For example, ./config/ processes only files in the config directory itself, not files in ./config/jwt/ or other subdirectories. ::: +:::info +Not to be confused with the [`zsc env-replace`](/references/zsc#env-replace) command, which renders `{{.VARIABLE}}` Go templates from a source path into a separate target path and can be run from `initCommands` or manually. +::: + **How it works:** 1. Define placeholders in your files using the specified delimiters 2. Set environment variables with matching names @@ -653,12 +684,13 @@ The placeholder gets replaced with the actual JWT public key during deployment. ### routing *[Optional]* -Configures URL routing, redirects, and HTTP headers (only for Static services). +Configures the document root, redirects, CORS and HTTP headers of the [Static service](/static/overview#routing--configuration). Any other service, including the Nginx service, ignores this section without an error. ```yaml run: + base: alpine/static routing: - root: /custom/root + root: dist cors: "'*' always" redirects: - from: /old-path @@ -673,31 +705,31 @@ run: Available parameters: #### root *[Optional]* -Sets a custom root directory for the service. +Sets the folder served by the service, relative to `/var/www` (default `/var/www` itself). This is the Static service's equivalent of `documentRoot`. - Type: `string` #### cors *[Optional]* Enables CORS headers for cross-origin requests. - Type: `string` -- Sets `Access-Control-Allow-Origin`, `Access-Control-Allow-Methods`, `Access-Control-Allow-Headers`, and `Access-Control-Expose-Headers` +- Sets `Access-Control-Allow-Origin`, `Access-Control-Allow-Methods`, `Access-Control-Allow-Headers`, and `Access-Control-Expose-Headers`, all to the same value - Special case: `"*"` is automatically converted to `'*'` #### redirects *[Optional]* Defines URL redirects and rewrites. - Type: `array of objects` - Each redirect object supports: - - **from** *[Required]* - Source path to match ([supports wildcards](/static/overview#wildcard-matching) with `*`) - - **to** *[Required]* - Destination path - - **status** *[Optional]* - HTTP status code (required for absolute URLs) - - **preservePath** *[Optional]* - Preserve path after wildcard match - - **preserveQuery** *[Optional]* - Preserve query parameters + - **from** *[Required]* - Source path to match. Without `*` it matches the exact path, with a trailing `*` it matches the path prefix ([path matching](/static/overview#path-matching)). An absolute URL (`https://old-domain.com/*`) matches on the request domain. + - **to** *[Required]* - Destination path or absolute URL + - **status** *[Optional]* - HTTP status code. Omit it for a masked redirect that serves the target content under the original URL. Required for absolute URLs, and limited to `301` or `302` when `from` is absolute. + - **preservePath** *[Optional]* - Append the part of the path after the wildcard to `to`. Not allowed on masked redirects. + - **preserveQuery** *[Optional]* - Append the original query string. Not allowed on masked redirects. #### headers *[Optional]* Sets custom HTTP headers for specific paths. - Type: `array of objects` - Each header object supports: - - **for** *[Required]* - Path pattern to match - - **values** *[Required]* - Object with header name/value pairs + - **for** *[Required]* - Path to match, same rules as `from` (`"/*"` for the whole site, `"/"` matches the homepage only) + - **values** *[Required]* - Object with header name/value pairs. Values are inserted into `add_header` verbatim, so include the quotes: `X-Frame-Options: "'DENY'"` **Example usage:** @@ -5180,7 +5212,7 @@ Stuck, or want to share what you built? Our core team and community are on Disco # Static > Overview -The Static service provides a way to serve static content through a pre-configured Nginx setup. It balances simplicity with the flexibility needed for modern web applications. +The Static service serves static files (HTML, CSS, JavaScript, images, the build output of any frontend framework) through a pre-configured Nginx. You describe redirects, headers and CORS declaratively in `zerops.yaml` and Zerops generates the Nginx configuration for you. ### Experience the simplicity of Zerops @@ -5188,49 +5220,126 @@ Deploy an Analog app with static hosting in seconds. All you need is a Zerops ac [Deploy "analog" recipe on Zerops](https://app.zerops.io/recipe/?lf=analog) +## Static or Nginx service? + +Zerops offers two services for static content. Both run Nginx, but they are configured differently and support different `zerops.yaml` attributes: + +| | Static service | [Nginx service](/nginx/overview) | +|---|---|---| +| `run.base` | `alpine/static` or `ubuntu/static` | `alpine/nginx@latest` or `ubuntu/nginx@latest` | +| Nginx configuration | Generated from `run.routing` | Built-in default, or your own file via `run.siteConfigPath` | +| Redirects, custom headers, CORS | Declarative `run.routing` | Written by you in the Nginx configuration | +| Fallback for missing paths | `/index.html` (SPA friendly), plus extensionless `.html` pages | `/index.html` | +| Prerender.io for crawlers | Built in, enabled by `PRERENDER_TOKEN` | Not built in | +| Reverse proxy (`proxy_pass`), caching, rate limiting, other ports | Not available | Anything Nginx can do, in your own configuration | +| Document root | `run.routing.root` | `run.documentRoot` | +| Document root | `run.routing.root` | `run.documentRoot` | + +Choose **Static** when you deploy framework build output or plain files and need at most redirects, headers and CORS. Choose **Nginx** when you need Nginx directives that `routing` cannot express, typically proxying to another service, response caching, or listening on additional ports. + +:::caution Attributes are not interchangeable +- `run.routing` is read only by the Static service. On any other service, including Nginx, it is silently ignored. +- `run.documentRoot` is ignored by the Static service. Use `run.routing.root` instead. +::: + +:::tip Sending paths to another service +You do not need an Nginx service just to send `/api` to a backend. [Domain access routing](/references/networking/public-access#http-routing-setup) on the project's L7 balancer maps public paths to services and ports, so a Static frontend and an API can share one domain. +::: + ## Quick Start -Add a Static service to your project by including this in your `zerops.yaml`: +Build your frontend with any runtime and hand the output to the Static service: ```yaml title="zerops.yaml" zerops: - setup: app + build: + base: nodejs@latest + buildCommands: + - npm i + - npm run build + deployFiles: + - dist/~ # deploy the *contents* of dist to /var/www run: - os: alpine - base: static + base: alpine/static ``` -## Routing & Configuration +The `~` in `dist/~` deploys the contents of `dist` directly into `/var/www`, so `index.html` ends up at the document root. If you deploy the folder as a whole (`deployFiles: - dist`), point the service at it with [`routing.root: dist`](#document-root). + +If your files are already built, skip the `build` section and [deploy them with zCLI](/references/cli). + +The base carries the operating system: `alpine/static` or `ubuntu/static`. The bare shorthand `static` is accepted and means `alpine/static`. + +## How requests are served -### Default Behavior +The generated configuration serves files from the [document root](#document-root) and resolves every request in this order: -Every Static service in Zerops comes with built-in defaults optimized for modern web applications, including Single Page Applications. By default, for any incoming request, the service will: -1. Try to serve the exact path (`$uri`) -2. Try with .html extension (`$uri.html`) -3. Look for an index.html in the directory (`$uri/index.html`) -4. Fall back to `/index.html` (suitable for SPAs) -5. Return 404 if none of the above exist +1. The exact path (`$uri`) +2. The path with `.html` appended (`$uri.html`), so `/about` serves `about.html` +3. `index.html` inside the directory (`$uri/index.html`), so `/docs` serves `docs/index.html` +4. `/index.html`, which makes client-side routing of Single Page Applications work +5. HTTP 404 if `/index.html` does not exist either + +Other built-in behavior: + +- The service listens on port 80 only. HTTPS is terminated on the Zerops balancer and forwarded as plain HTTP. +- Everything under `/.git` returns 404. +- Gzip compression is enabled for text-based content types. +- Prerender.io is wired in and becomes active once `PRERENDER_TOKEN` is set, see [SEO with Prerender](#seo-with-prerender). :::important SPAs -Single Page Applications work out of the box without any additional configuration. The built-in fallback to `/index.html` ensures that client-side routing functions properly. +Single Page Applications work out of the box. No redirects are needed for client-side routing. A consequence of the fallback is that a request for a nonexistent path returns `/index.html` with status 200, not 404. ::: -### Custom Routing Configuration +## Document root + +By default files are served from `/var/www`, the folder your `deployFiles` land in. To serve a subfolder, set `run.routing.root`. The path is relative to `/var/www`: -The Static service allows you to configure additional URL routing and redirects through simple YAML configuration, abstracting away the complexity of Nginx configuration. +```yaml title="zerops.yaml" +run: + base: alpine/static + routing: + root: dist # serves /var/www/dist +``` + +:::caution +`run.documentRoot` has no effect on the Static service. It is only used by the [Nginx](/nginx/how-to/build-pipeline#documentroot) and PHP services (and inside a [custom `.tmpl` configuration](#custom-nginx-configuration)). +::: -Configure custom routing beyond the default behavior in the `run.routing` section of your `zerops.yaml`: +## Routing & Configuration + +Configure redirects, headers and CORS in the `run.routing` section of your `zerops.yaml`: ```yaml title="zerops.yaml" run: + base: alpine/static routing: + root: dist + cors: "*" redirects: - # Only needed for custom redirect scenarios - from: /special-path/* to: /specific-landing-page status: 302 + headers: + - for: "/*" + values: + X-Frame-Options: "'DENY'" ``` +Zerops turns this into `location` blocks inside the generated Nginx configuration. Every deploy regenerates the configuration, so `routing` changes take effect on the next deploy. + +### Path matching + +The `from` field of a redirect and the `for` field of a header rule use the same matching rules: + +- **Without a wildcard** (`/about`) the rule matches that exact path only. `/about?x=1` matches (the query string is not part of the path), `/about/` and `/about/team` do not. +- **With a trailing `*`** (`/blog/*`) the rule matches the path prefix. `/blog/`, `/blog/post` and `/blog/2024/post.html` all match. `/blog` without the trailing slash does not. +- `*` is only supported at the end of a path. Patterns such as `/*.html` or `/*/edit` are not supported. +- When several rules match, an exact rule wins over a prefix rule, and the longest matching prefix wins among prefix rules. +- Path rules apply to paths only. Redirects between domains use [absolute redirects](#absolute-redirects), which are evaluated before any path rule. + +`/*` matches everything and is the rule to use when you want a header on all responses. + ### Redirects #### Relative Redirects @@ -5239,7 +5348,7 @@ run: Remember that SPA routing is already built into the default behavior. You don't need to add any custom redirects for client-side routing to work. ::: -Use relative redirects to route paths within your application. When both `from` and `to` are relative paths, you can omit the `status` code to create a masked redirect that shows the content of the target page while preserving the original URL: +When both `from` and `to` are paths, the redirect is relative. Omitting `status` creates a **masked redirect**: the content of the target is served while the URL in the browser stays the same. With a `status`, the browser receives an HTTP redirect to the target: ```yaml title="zerops.yaml" routing: @@ -5267,35 +5376,56 @@ routing: status: 302 ``` -:::caution Important -When using `preservePath` with wildcards, ensure the `to` path ends with a `/` to maintain proper path concatenation. For example, `/blog/*` to `/new-blog/` will correctly redirect `/blog/hello.html` to `/new-blog/hello.html`, while `/new-blog` would result in `/new-bloghello.html`. -::: +- `status` can be any redirect code, typically `301`, `302`, `307` or `308`. +- `preservePath` appends the part of the path after the wildcard to `to`. `/blog/*` to `/articles/` redirects `/blog/hello.html` to `/articles/hello.html`. End `to` with a `/`, otherwise the result is `/articleshello.html`. +- `preserveQuery` appends the original query string. Without it the query string is dropped. +- A masked redirect serves the target through the [default rules](#how-requests-are-served), so `to: /about-us` may resolve to `about-us.html` or `about-us/index.html`. +- `preservePath` and `preserveQuery` are not allowed on masked redirects. Setting them fails the deploy with `Preserve path must not be set for masked redirects`. A masked redirect for a prefix (`from: /legacy/*`) simply serves the same `to` for every matching path. #### Absolute Redirects -For redirecting between domains or to external URLs, use absolute redirects by including `http://` or `https://`. When using absolute URLs in either `from` or `to`, you must specify a `status` code: +Use absolute URLs (`http://` or `https://`) to redirect between domains or to an external site. Absolute redirects require a `status`. + +A redirect with an **absolute `to`** and a path `from` works like a relative redirect, only the destination is external. Any status code is allowed: + +```yaml title="zerops.yaml" +routing: + redirects: + # /docs/getting-started -> https://docs.example.com/getting-started + - from: /docs/* + to: https://docs.example.com/ + status: 301 + preservePath: true +``` + +A redirect with an **absolute `from`** matches on the domain of the request, which is why it only makes sense for domains that are [pointed at this service](/references/networking/public-access#custom-domain-access). It is evaluated before any path rule and supports `status` `301` or `302` only: ```yaml title="zerops.yaml" routing: redirects: - # Redirect an old domain to a new one + # Redirect an old domain to a new one, keeping the query string - from: https://old-domain.com/* to: https://new-domain.com status: 301 - preserveQuery: true # Optional: maintain query parameters + preserveQuery: true - # Redirect with path preservation + # Redirect with path preservation: https://old-site.com/blog/x -> https://new-site.com/blog/x - from: https://old-site.com/* - to: https://new-site.com/ + to: https://new-site.com status: 301 preservePath: true ``` +- With an absolute `from`, `preservePath` appends the complete request path (it always starts with `/`), so do **not** end `to` with a `/`. +- The domain match is a case-insensitive substring match on `host + path`. `https://old-domain.com/*` therefore also matches `www.old-domain.com` and every other subdomain of `old-domain.com`. +- `https://*.old-domain.com/*` matches subdomains of `old-domain.com` only, not `old-domain.com` itself. +- `https://old-domain.com/` without the trailing `*` matches the root path of that domain only. + #### Wildcard Matching -Use `*` as a wildcard in your paths: -- **At the end of a path**: Matches any subsequent content -- **At the start of a domain** (after `https://`): Enables regex matching for subdomains +Use `*` as a wildcard: +- **At the end of a path** it matches the path prefix, see [path matching](#path-matching). +- **At the start of a domain** in an absolute `from` (`https://*.domain.com/*`) it matches all subdomains. Example of domain management: @@ -5303,12 +5433,12 @@ Example of domain management: run: routing: redirects: - # Redirect a specific domain to an article + # Redirect a specific domain (and its subdomains) to an article - from: https://promo-domain.com/* to: https://main-site.com/special-offer status: 302 - # Redirect all subdomains to main site + # Redirect only subdomains of old-domain.com to the main site - from: https://*.old-domain.com/* to: https://main-site.com status: 302 @@ -5316,11 +5446,12 @@ run: #### Matching Priority -When multiple redirects are configured, they follow Nginx's matching priority system: +Rules are matched in this order: -1. Exact matches are checked first -2. Simple path matches (without wildcards) are checked next -3. Pattern matches (with wildcards) are checked last +1. Absolute redirects (matched on the request domain) +2. Exact path rules (`from` without a wildcard) +3. Prefix rules (`from` ending with `*`), longest prefix first +4. The [default behavior](#how-requests-are-served) for everything else For example: @@ -5332,11 +5463,11 @@ routing: to: /home status: 302 - # Simple path match - masked redirect + # Exact match - masked redirect - from: /about to: /about-us - # Pattern match with path preservation + # Prefix match with path preservation - from: /blog/* to: /articles/ preservePath: true @@ -5344,10 +5475,11 @@ routing: ``` In this configuration: -- `/` will redirect to `/home` with a 302 status -- `/about` will show content from `/about-us` but keep the URL as `/about` -- `/blog/post-123.html` will redirect to `/articles/post-123.html` -- Any other path will use the [default behavior](#default-behavior) +- `/` redirects to `/home` with a 302 status +- `/about` shows content from `/about-us` but keeps the URL as `/about` +- `/about/` and `/about/team` do not match the exact rule and use the default behavior +- `/blog/post-123.html` redirects to `/articles/post-123.html` +- Any other path uses the [default behavior](#how-requests-are-served) #### Common Redirect Patterns @@ -5358,8 +5490,10 @@ routing: - from: https://old-domain.com/* to: https://new-domain.com status: 301 + preservePath: true + preserveQuery: true ``` -Use permanent (301) redirects when permanently moving content to maintain SEO value. +Use permanent (301) redirects when permanently moving content to maintain SEO value. Both `preservePath` and `preserveQuery` keep the visitor on the same page of the new domain. **Multiple Domain Management** ```yaml title="zerops.yaml" @@ -5376,12 +5510,23 @@ run: to: https://main-site.com/campaign status: 302 - # Legacy domains and subdomains + # Legacy subdomains - from: https://*.legacy-domain.com/* to: https://main-site.com status: 302 ``` +**Moving a section of the site** +```yaml title="zerops.yaml" +routing: + redirects: + - from: /blog/* + to: /articles/ + status: 301 + preservePath: true + preserveQuery: true +``` + ### CORS Configuration You can enable CORS for your static service by adding a `cors` directive: @@ -5396,25 +5541,27 @@ run: cors: "'*' always" ``` -The `cors` directive sets the following headers: +The `cors` directive sets the following headers on every response, including redirects: - `Access-Control-Allow-Origin` - `Access-Control-Allow-Methods` - `Access-Control-Allow-Headers` - `Access-Control-Expose-Headers` +All four headers receive the same value. If you need different values per header, set them individually with the [`headers`](#custom-headers) directive instead. + :::note The `cors` directive has a special case: if you specify just `"*"`, it's automatically converted to `'*'`. For any other values, you need to include the proper Nginx syntax including quotes. ::: ### Custom Headers -For more control over HTTP headers, use the `headers` directive: +For more control over HTTP headers, use the `headers` directive. The `for` field uses the same [path matching](#path-matching) as redirects, so use `"/*"` to cover the whole site. `"/"` alone would match the homepage only: ```yaml title="zerops.yaml" run: routing: headers: - - for: "/" + - for: "/*" values: # All values need proper quoting since they're inserted directly into Nginx X-Frame-Options: "'DENY'" @@ -5436,7 +5583,7 @@ Here are examples for different header scenarios: ```yaml title="zerops.yaml" headers: - - for: "/" + - for: "/*" values: # Simple header with proper quoting X-Frame-Options: "'DENY'" @@ -5464,9 +5611,12 @@ add_header Content-Security-Policy "default-src 'self' https://cdn.example.com; ``` :::important Path Handling -When you specify headers for a path that doesn't have an existing location block, the Static service automatically creates a location with the same [default behavior](#default-behavior) as the root path (trying files in order: `$uri`, `$uri.html`, `$uri/index.html`, `/index.html` or returning 404). +Headers are attached to the matched location, and only the single best-matching rule applies to a request. Rules are not merged: -If you add headers for a path that already has a location block, your headers will be merged with the existing configuration. +- A request for `/docs/page` with rules for `/*` and `/docs/*` receives only the `/docs/*` headers. Repeat the site-wide headers in the more specific rule if you need both. +- A redirect defined in `redirects` does not pick up headers from a broader rule such as `/*`, only the [`cors`](#cors-configuration) headers. To add headers to a redirect response, add a header rule with the same `for` path as the redirect's `from`. The two are merged. +- A header rule for a path without a redirect serves files with the same [default behavior](#how-requests-are-served) as the rest of the site. +- Without `always`, Nginx adds a header only to 2xx, 3xx and 304 responses, so use `always` for headers that must be present on error pages too. ::: ### Combining CORS and Custom Headers @@ -5478,7 +5628,7 @@ run: routing: cors: "'*' always" headers: - - for: "/" + - for: "/*" values: X-Frame-Options: "'DENY'" ``` @@ -5493,12 +5643,14 @@ If you specify Access-Control headers in the `headers` directive, they will over Single Page Applications render content with JavaScript, which most crawlers can't process—they see an empty page instead of your content. This affects traditional search engines, social media platforms, and AI tools like ChatGPT, Perplexity, and Claude. -The Static service includes built-in support for Prerender.io, which automatically detects crawlers and serves them pre-rendered HTML while your users get the full interactive experience. +The Static service includes built-in support for [Prerender.io](https://prerender.io), which automatically detects crawlers (search engines, social media link previews, SEO tools and AI bots) and serves them pre-rendered HTML while your users get the full interactive experience. Requests for assets such as scripts, styles and images are never prerendered. ### Setup -1. Set the `PRERENDER_TOKEN` secret variable with your Prerender.io token -2. The service automatically configures necessary rewrites based on user agents +1. Set the `PRERENDER_TOKEN` [secret variable](/features/env-variables) with your Prerender.io token +2. Restart the service (or trigger a new deploy). The Nginx configuration is generated when a container starts, so it picks the token up on the next start + +No changes to `zerops.yaml` are needed. ### Custom Prerender Host @@ -5507,7 +5659,7 @@ If you're using a custom Prerender host, add it to environment variables in `zer ```yaml title="zerops.yaml" run: envVariables: - - PRERENDER_HOST=your.prerender.host + PRERENDER_HOST: your.prerender.host ``` :::note Default @@ -5529,10 +5681,10 @@ zerops: deployFiles: - dist/~ # Your framework's output directory run: - base: static + base: alpine/static ``` -The key is pointing `deployFiles` to wherever your framework outputs its built files (`dist/`, `build/`, `.output/public/`, etc.). +The key is pointing `deployFiles` to wherever your framework outputs its built files (`dist/`, `build/`, `.output/public/`, etc.). The trailing `/~` deploys the folder's contents to the document root. This configuration: 1. Uses Node.js for building the application @@ -5548,6 +5700,20 @@ For framework-specific examples, check out our [recipe collection](https://githu ## Advanced Topics +### Custom Nginx configuration + +The Static service also accepts your own Nginx configuration through `run.siteConfigPath`, the same attribute the [Nginx service](/nginx/how-to/customize-web-server) uses. The configuration is chosen with this precedence: + +1. `run.routing` is set: the configuration is generated from it and `siteConfigPath` is ignored +2. Only `run.siteConfigPath` is set: your file is used as the complete `server` configuration +3. Neither is set: the generated default configuration is used + +A `.tmpl` file is rendered as a Go template with `{{.DocumentRoot}}` (the value of `run.documentRoot`, `/var/www` when unset) and `{{.Environment.NAME}}` for environment variables. Any other extension is copied verbatim. See the [Nginx service guide](/nginx/how-to/customize-web-server#customize-nginx-configuration) for the requirements a custom configuration must meet. + +:::tip +If you find yourself writing a custom configuration, consider switching to the [Nginx service](/nginx/overview). It is the same Nginx with `documentRoot` and `siteConfigPath` as first-class options, and it is what the rest of the documentation assumes for hand-written configurations. +::: + ### Switching to Full Nginx If you need more control over your Nginx configuration: @@ -5558,6 +5724,10 @@ If you need more control over your Nginx configuration: 4. Copy the generated Nginx configuration 5. Use this configuration as a starting point for a full Nginx service +The same file is available inside any running container of the service at `/etc/nginx/sites-enabled/default.site`, for example over [SSH](/references/networking/ssh). + +To migrate, change `run.base` to `alpine/nginx@latest` (or `ubuntu/nginx@latest`), replace `run.routing` with `run.siteConfigPath` pointing at the copied configuration (with the `root` directive adjusted or replaced by `{{.DocumentRoot}}`), and remove `routing`, since the Nginx service ignores it. Prerender.io support is part of the generated configuration and will be carried over with it. + :::tip This allows you to move to a more customizable setup while maintaining your existing routing logic. ::: @@ -5580,7 +5750,7 @@ run: to: https://main-site.com/campaign status: 302 - # Legacy domains and subdomains + # Legacy subdomains - from: https://*.legacy-domain.com/* to: https://main-site.com status: 302 @@ -5590,7 +5760,7 @@ run: ### Development Setup -Configuration for local development with CORS and API proxying: +Configuration for a development environment with CORS and an API on another domain: ```yaml title="zerops.yaml" run: @@ -5598,12 +5768,18 @@ run: # CORS with proper quoting cors: "'*' always" redirects: - # API requests + # Send browsers calling /api/... to the API domain - from: /api/* - to: https://api.your-domain.com - status: 302 + to: https://api.your-domain.com/ + status: 307 + preservePath: true + preserveQuery: true ``` +:::note +This is a browser redirect, not a reverse proxy. The Static service cannot proxy requests. To serve an API under the same domain as the frontend, use [domain access routing](/references/networking/public-access#http-routing-setup) or the [Nginx service](/nginx/overview) with a `proxy_pass` configuration. +::: + ### Production Setup with Security Security-enhanced configuration for production environments: @@ -5612,7 +5788,7 @@ Security-enhanced configuration for production environments: run: routing: headers: - # Custom headers for default location + # Custom headers for the whole site - for: "/*" values: X-Frame-Options: "'DENY' always" @@ -5621,6 +5797,7 @@ run: Content-Security-Policy: '"default-src ''self''" always' ``` + ---------------------------------------- # Shared Storage > Overview @@ -15819,7 +15996,11 @@ Read more about how the [readiness check works](/nodejs/how-to/deploy-process#re # Nginx > Overview -The Nginx static service contains the [Nginx ↗](https://nginx.org/) web server optimized for your static content. +The Nginx static service contains the [Nginx ↗](https://nginx.org/) web server optimized for your static content. You get a plain Nginx with a sensible default configuration and full control over it through your own configuration file. + +:::tip Static or Nginx? +Zerops also offers the [Static service](/static/overview), which generates the Nginx configuration for you from a declarative `run.routing` section (redirects, headers, CORS) and has Prerender.io built in. Choose the Nginx service when you need to write Nginx configuration yourself, for example for `proxy_pass`, caching, or additional ports. The `run.routing` section is ignored by the Nginx service, and `run.documentRoot` is ignored by the Static service. See the [comparison](/static/overview#static-or-nginx-service). +::: ## How to start @@ -15830,7 +16011,7 @@ The Nginx static service contains the [Nginx ↗](https://nginx.org/) web server - [Create Nginx static service](/nginx/how-to/create) — Start with creating a Nginx static service using GUI or zCLI. - [zerops.yaml](/nginx/how-to/build-pipeline#add-zeropsyaml-to-your-repository) — See a full example of zerops.yaml file to create your own app. - [Scaling configuration](/nginx/how-to/scaling) — Set up scaling of your Nginx application so that it runs smoothly while using only necessary resources. -- [SEO Optimization](/nginx/how-to/env-variables#prerenderio-support) — Built-in prerender.io support for better SEO. +- [Custom Nginx configuration](/nginx/how-to/customize-web-server) — Bring your own Nginx configuration with siteConfigPath. {" "} @@ -15859,9 +16040,17 @@ Have you build something that others might find useful? Don't hesitate to share # Nginx > Faq + **Question: What is the difference between the Nginx service and the Static service?** + +Both serve static files with Nginx. The [Static service](/static/overview) generates the Nginx configuration from a declarative `run.routing` section (redirects, headers, CORS) and has Prerender.io built in. The Nginx service gives you the default configuration or your own file via `run.siteConfigPath`, which is the way to go for `proxy_pass`, caching or extra ports. See the [comparison](/static/overview#static-or-nginx-service). + + **Question: Why is my run.routing section ignored?** + +`run.routing` is only read by the Static service. The Nginx service ignores it without an error. Either switch `run.base` to `alpine/static`, or express the redirects and headers in a custom configuration set via [`siteConfigPath`](/nginx/how-to/customize-web-server). + **Question: How do I enable SEO optimization with prerender.io?** -Zerops provides built-in prerender.io support. Simply set the `PRERENDER_TOKEN` environment variable with your prerender.io service token. See our [prerender.io documentation](/nginx/how-to/env-variables#prerenderio-support) for details. +Prerender.io support is built into the [Static service](/static/overview#seo-with-prerender), not into the Nginx service. On the Nginx service you can add the prerender rules to your own configuration set via [`siteConfigPath`](/nginx/how-to/customize-web-server), or switch to the Static service. ---------------------------------------- @@ -15899,48 +16088,14 @@ Zerops provides built-in prerender.io support. Simply set the `PRERENDER_TOKEN` # Nginx > How To > Env Variables -## Prerender.io Support - -Zerops provides built-in prerender.io support for SEO optimization. Configure it using these environment variables: +## Environment variables in the Nginx configuration - - - - - - - - - - - - - - - - - - - - - - - -
VariableRequiredDescriptionDefault
PRERENDER_TOKENYesYour prerender.io service token-
PRERENDER_HOSTNoPrerender service hostservice.prerender.io
+Nginx itself does not read environment variables. To use them in your configuration, deploy a `.tmpl` file via [`siteConfigPath`](/nginx/how-to/customize-web-server#customize-nginx-configuration) and reference variables as `{{.Environment.NAME}}`. Zerops renders the template every time a container starts. -:::tip -Set `PRERENDER_TOKEN` as a secret environment variable in Zerops GUI for security. +:::note +Prerender.io support (`PRERENDER_TOKEN`, `PRERENDER_HOST`) is a feature of the [Static service](/static/overview#seo-with-prerender). The Nginx service does not act on these variables. ::: -Example in zerops.yaml: -```yaml -zerops: - - setup: app - run: - envVariables: - PRERENDER_HOST: "custom.prerender.host" -``` - ---------------------------------------- # Nginx > How To > Deploy Process @@ -15964,6 +16119,11 @@ server { server_name _; root {{.DocumentRoot}}; + location ^~ /.git { + deny all; + return 404; + } + location / { try_files $uri $uri/ /index.html; } @@ -15973,15 +16133,23 @@ server { } ``` -The configuration contains 2 variables: +The configuration contains one variable: - **`{{.DocumentRoot}}`** is replaced by the [`run.documentRoot`](/nginx/how-to/build-pipeline#documentroot) attribute from the `zerops.yaml`. If the attribute is not specified, the default value `/var/www` is used. +With this configuration a request is served from the exact path, then from `index.html` inside a directory (a request for `/docs` is redirected to `/docs/` first), and falls back to `/index.html` for everything else, which makes client-side routing of Single Page Applications work. Extensionless URLs such as `/about` for `about.html` are **not** resolved, and anything under `/.git` returns 404. Requests for a nonexistent path return `/index.html` with status 200, not 404. + +The surrounding `nginx.conf` enables gzip for text-based content types, hides the Nginx version and allows request bodies up to 10 GB. + +:::note +Redirects, custom headers and CORS have no declarative configuration on the Nginx service. Write them in your own configuration as described below. The `run.routing` section of `zerops.yaml` belongs to the [Static service](/static/overview#routing--configuration) and is ignored here. +::: + ## Customize Nginx configuration Follow these steps to customize the Nginx configuration in Nginx static service: -1. Create a **.tmpl** file with the Nginx configuration in your repository. +1. Create a **.tmpl** file with the Nginx configuration in your repository. The file replaces the whole default `server` block shown above, so start from a copy of it. 2. Optionally use following variables: @@ -15995,8 +16163,16 @@ root {{.DocumentRoot}}; - **`{{.Environment.ENV_NAME}}`** is replaced by the [env variable](/nginx/how-to/env-variables) value. The env variable must be either defined in [run.envVariables](/nginx/how-to/build-pipeline#envvariables) in `zerops.yaml` or set as a [secret](/nginx/how-to/env-variables#set-secret-env-variables-in-zerops-gui) or [generated](/nginx/how-to/env-variables#generated-env-variables) env variable in Zerops GUI. +Example: + +``` +location /api/ { + proxy_pass http://{{.Environment.API_HOSTNAME}}:3000/; +} +``` + :::caution -Use the **.tmpl** file extension to make Zerops interpret the file as a template. Zerops will replace the supported variables listed above. +Use the **.tmpl** file extension to make Zerops interpret the file as a template. Zerops will replace the supported variables listed above. A file with any other extension is copied to Nginx verbatim, without variable replacement. ::: 3. Check that your Nginx configuration is consistent with Zerops requirements: @@ -16004,6 +16180,7 @@ Use the **.tmpl** file extension to make Zerops interpret the file as a template - Do not use IP addresses in the `listen` directive - If you use other ports than `:80` in the `listen` directive, add them to the [`run.ports`](/nginx/how-to/build-pipeline#ports) in your `zerops.yaml` as well. - Do not use the port **:443**. All the incoming `https://` traffic is terminated on the Zerops internal balancer where the SSL certificate is installed and the request is forwarded to your Nginx static service as a **http://** on the port **:80**. +- Keep the `access_log` and `error_log` directives from the default configuration so that the logs show up in Zerops. 4. Add the [`siteConfigPath`](/nginx/how-to/build-pipeline#siteconfigpath) to the run section of your `zerops.yaml` @@ -16016,40 +16193,76 @@ zerops: # REQUIRED. Set the base technology for the build environment: base: nodejs@latest + buildCommands: + - npm i + - npm run build + # REQUIRED. Select which files / folders to deploy after # the build has successfully finished deployFiles: - - vendor - - public + - dist + - site_config.tmpl # ==== how to run your application ==== run: - documentRoot: public + base: alpine/nginx@latest - # OPTIONAL. Sets the custom Nginx or Apache configuration. The file must be deployed in the runtime container. Enter the path to the file relative to the /var/www folder + # OPTIONAL. Folder served by Nginx, relative to /var/www + documentRoot: dist + + # OPTIONAL. Sets the custom Nginx configuration. The file must be deployed in the runtime container. Enter the path to the file relative to the /var/www folder siteConfigPath: site_config.tmpl ``` 5. Ensure that the [`build.deployFiles`](/nginx/how-to/build-pipeline#deployfiles) contains the folder with the `siteConfigPath` or add the path to the Nginx config file to the `deployFiles` list. Zerops will deploy the file to the runtime container(s). -6. [Trigger](/nginx/how-to/trigger-pipeline) the build & deploy pipeline. +:::tip +Everything under the document root is publicly served, including a configuration file deployed there. Keep the configuration outside the `documentRoot` folder (as in the example above, where `documentRoot` is `dist` and the template sits next to it in `/var/www`). +::: + +6. [Trigger](/nginx/how-to/trigger-pipeline) the build & deploy pipeline. The configuration is applied every time a container starts, and the deploy fails if the file is missing. -## SEO & Prerender Support +## Common customizations -Single Page Applications and JavaScript-heavy sites render content client-side, which most crawlers can't process—they see an empty page instead of your content. This affects traditional search engines, social media platforms, and AI tools like ChatGPT, Perplexity, and Claude. +Serve extensionless `.html` pages and keep the SPA fallback: -### Built-in Prerender.io Integration +``` +location / { + try_files $uri $uri.html $uri/index.html /index.html =404; +} +``` -The default Nginx configuration includes automatic [Prerender.io](https://prerender.io) support. When enabled, it detects crawler requests (including AI crawlers) and serves them pre-rendered HTML while your users get the full interactive experience. +Redirect an old path permanently: -### Setup +``` +location = /old-page { + return 301 /new-page; +} +``` + +Add headers to every response: + +``` +add_header X-Frame-Options 'DENY' always; +add_header X-Content-Type-Options 'nosniff' always; +``` + +Proxy a path to another service of the project (the hostname is the other service's name): -To enable prerender support: +``` +location /api/ { + proxy_pass http://api:3000/; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +} +``` -1. Set the `PRERENDER_TOKEN` environment variable with your Prerender.io token (see [environment variables](/nginx/how-to/env-variables#prerenderio-support)) -2. Optionally set `PRERENDER_HOST` if using a custom prerender server +## SEO & Prerender Support + +Prerender.io support is not part of the default Nginx service configuration. It is built into the [Static service](/static/overview#seo-with-prerender), which enables it as soon as `PRERENDER_TOKEN` is set. + +If you need prerendering together with a hand-written Nginx configuration, either start from the configuration generated by a Static service (open the Static service in the GUI and choose **Need to switch to full Nginx service?**, or copy `/etc/nginx/sites-enabled/default.site` from one of its containers) or add the [Prerender.io Nginx snippet](https://docs.prerender.io/docs/nginx-2) to your own template. -The Nginx configuration will automatically handle the rest—no additional configuration needed. ---------------------------------------- @@ -16070,10 +16283,10 @@ You should not include your application code in the custom runtime image, as you The default Nginx Static runtime environment contains: -- {data.alpine.default} +- {data.alpine.default} with `alpine/nginx@latest`, or Ubuntu with `ubuntu/nginx@latest` - Selected version of Nginx when the runtime service was created - [zCLI](/references/cli) -- Git +- Git, wget and curl ### When You Need a Custom Runtime Image @@ -16083,6 +16296,8 @@ Here are Nginx Static-specific examples of configuring custom runtime images in ### Basic Nginx Static Setup +Use `sudo apk add` on Alpine and `sudo apt-get install -y` on Ubuntu. + For complete configuration details, see the [runtime prepare phase configuration guide](/features/pipeline#configuration). ## Process and Caching @@ -16111,6 +16326,10 @@ If your `prepareCommands` fail, check the [prepare runtime log](/nginx/how-to/lo The Nginx static service contains the Nginx web server optimized for your static content. Nginx static service is highly scalable and customisable to suit both development and production. +:::tip +If you only need redirects, headers and CORS on top of your static files, the [Static service](/static/overview) configures them declaratively without any Nginx configuration. See [Static or Nginx?](/static/overview#static-or-nginx-service). +::: + ## Create Nginx static service using Zerops GUI First, set up a project in Zerops GUI. Then go to the project dashboard page and choose **Add new service** in the left menu in the **Services** block. Then add a new Nginx static service: @@ -16171,11 +16390,11 @@ project: services: - # service name hostname: app - # service type and version number in nginx@{version} format - type: nginx@latest + # service type, OS and version number in {os}/nginx@{version} format + type: ubuntu/nginx@latest # defines the minimum number of containers for horizontal autoscaling minContainers: 1 - # defines the maximum number of containers for horizontal autoscaling. Max value = 6. + # defines the maximum number of containers for horizontal autoscaling. maxContainers: 6 # optional: create env variables envSecrets: @@ -16183,7 +16402,11 @@ services: S3_ACCESS_SECRET: 'ogFthuiLYki8XoL73opSCQ' ``` -The yaml file describes your future project infrastructure. The project will contain one Nginx version 8.1 service with default [auto scaling](/nginx/how-to/scaling) configuration. Hostname will be set to "app", the internal port(s) the service listens on will be defined later in the [zerops.yaml](/nginx/how-to/build-pipeline#ports). Following secret env variables will be configured: +:::note Operating system +Always include the OS in the type: `alpine/nginx@latest` or `ubuntu/nginx@latest`, and use the same value as `run.base` in your `zerops.yaml`. A bare `nginx@latest` is resolved to Ubuntu here but to Alpine in `zerops.yaml`, so the first deploy would silently switch the service to another OS. +::: + +The yaml file describes your future project infrastructure. The project will contain one Nginx version 1.22 service with default [auto scaling](/nginx/how-to/scaling) configuration. Hostname will be set to "app", the internal port(s) the service listens on will be defined later in the [zerops.yaml](/nginx/how-to/build-pipeline#ports). Following secret env variables will be configured: ```env S3_ACCESS_KEY_ID="P8cX1vVVb" @@ -16209,8 +16432,8 @@ project: services: - # service name hostname: app - # service type and version number in nginx@{version} format - type: nginx@latest + # service type, OS and version number in {os}/nginx@{version} format + type: ubuntu/nginx@latest # optional: vertical auto scaling customization verticalAutoscaling: cpuMode: DEDICATED @@ -16223,9 +16446,9 @@ services: startCpuCoreCount: 3 minFreeRamGB: 0.5 minFreeRamPercent: 20 - # defines the minimum number of containers for horizontal autoscaling. Max value = 6. + # defines the minimum number of containers for horizontal autoscaling. minContainers: 2 - # defines the maximum number of containers for horizontal autoscaling. Max value = 6. + # defines the maximum number of containers for horizontal autoscaling. maxContainers: 4 # optional: create secret env variables envSecrets: @@ -16422,11 +16645,11 @@ project: services: - # service name hostname: app - # service type and version number in nginx@{version} format - type: nginx@latest + # service type, OS and version number in {os}/nginx@{version} format + type: ubuntu/nginx@latest # defines the minimum number of containers for horizontal autoscaling minContainers: 1 - # defines the maximum number of containers for horizontal autoscaling. Max value = 6. + # defines the maximum number of containers for horizontal autoscaling. maxContainers: 6 # optional: create env variables envSecrets: @@ -16473,6 +16696,10 @@ Maximum size of the import.yaml file is 100 kB. Zerops provides a customizable build and runtime environment for your static content. +:::note Nginx service vs Static service +This page covers the Nginx service (`run.base: alpine/nginx@latest` or `ubuntu/nginx@latest`). The [Static service](/static/overview) (`run.base: alpine/static`) uses the same build and deploy pipeline but is configured through `run.routing` instead of `documentRoot` and `siteConfigPath`. The `run.routing` section is ignored by the Nginx service. See [Static or Nginx?](/static/overview#static-or-nginx-service). +::: + :::tip Two Deployment Approaches You can use the Nginx service in two ways: - **Runtime only**: Deploy pre-built static files directly (no build phase needed) @@ -16499,9 +16726,6 @@ zerops: # REQUIRED (if using build). Set the base technology for the build environment: base: nodejs@latest - # OPTIONAL. Set the operating system for the build environment. - # os: ubuntu - # OPTIONAL. Customize the build environment by installing additional packages # or tools to the base build environment. # prepareCommands: @@ -16526,8 +16750,8 @@ zerops: # ==== how to run your application ==== run: - # OPTIONAL. Sets the base technology for the runtime environment: - base: nginx@latest + # OPTIONAL. Sets the base technology and OS for the runtime environment: + base: alpine/nginx@latest # OPTIONAL. Customize the runtime Nginx environment by installing additional # dependencies to the base Nginx runtime environment. @@ -16603,14 +16827,16 @@ zerops: # ==== how to run your application ==== run: - # OPTIONAL. Sets the base technology for the runtime environment: - base: nginx@latest + # OPTIONAL. Sets the base technology and OS for the runtime environment: + base: alpine/nginx@latest ... ``` +Always use the OS-prefixed form (`alpine/nginx@latest`, `ubuntu/nginx@latest`). A bare `nginx@latest` is resolved differently in different places (Alpine in `zerops.yaml`, Ubuntu in an import YAML), so a deploy can silently switch the service to another OS. +

The base runtime environment contains {data.alpine.default}, the - selected major version of Nginx, [Zerops command line tool](/references/cli) and `composer`, `git` and `wget`. + selected major version of Nginx, [Zerops command line tool](/references/cli), `git`, `wget` and `curl`.

:::info @@ -16633,7 +16859,7 @@ zerops: run: # OPTIONAL. Sets the base technology for the runtime environment: base: - - nginx@latest + - alpine/nginx@latest prepareCommands: - zsc add go@latest ... @@ -16645,14 +16871,7 @@ To customize your build environment use the `prepareCommands` attribute. ### os -_OPTIONAL._ Sets the operating system for the runtime environment. - -Following options are available: - -- `alpine` -- `ubuntu` - -Default value is `alpine`. +_DEPRECATED._ The operating system is part of the `base` value: `alpine/nginx@latest` or `ubuntu/nginx@latest`. Do not set `os` separately. We are currently using following os version: @@ -16714,7 +16933,7 @@ _OPTIONAL._ Customizes the Nginx runtime environment by installing additional de

The base Nginx environment contains {data.alpine.default}, the selected - major version of Nginx, [Zerops command line tool](/references/cli) and `composer`, `git` and `wget`. To install + major version of Nginx, [Zerops command line tool](/references/cli), `git`, `wget` and `curl`. To install additional packages or tools add one or more prepare commands:

@@ -16769,7 +16988,7 @@ You can configure your prepare commands to be run in a single shell instance or

The prepare runtime container contains {data.alpine.default}, the - selected major version of Nginx, [Zerops command line tool](/references/cli) and `composer`, `git` and `wget`. + selected major version of Nginx, [Zerops command line tool](/references/cli), `git`, `wget` and `curl`.

The prepare runtime container does not contain your application code nor the built application. If you need to copy some folders or files from the build container to the runtime container (e.g. a configuration file) use the `addToRunPrepare` attribute in the build section of your chosen technology. @@ -16867,12 +17086,30 @@ zerops: _OPTIONAL._ Sets the custom Nginx configuration. :::info -If you don't set your custom configuratiin Zerops applies the [default](/nginx/how-to/customize-web-server#default-nginx-configuration) configuration. +If you don't set your custom configuration Zerops applies the [default](/nginx/how-to/customize-web-server#default-nginx-configuration) configuration. ::: -The file must be deployed in the runtime container. Enter the path to the file relative to the `/var/www` folder. +The file must be deployed in the runtime container. Enter the path to the file relative to the `/var/www` folder. A file with the `.tmpl` extension is rendered as a template (`{{.DocumentRoot}}`, `{{.Environment.NAME}}`), any other file is used verbatim. Read more about the [web server customization](/nginx/how-to/customize-web-server). +```yaml +zerops: + # hostname of your service + - setup: app + # ==== how to build your application ==== + build: + ... + deployFiles: + - dist + - site_config.tmpl + + # ==== how to run your application ==== + run: + documentRoot: dist + # OPTIONAL. Path to your Nginx configuration, relative to /var/www. + siteConfigPath: site_config.tmpl +``` + ### envVariables _OPTIONAL._ Defines the environment variables for the runtime environment. @@ -21183,7 +21420,7 @@ run: - litestream restore -if-replica-exists -if-db-not-exists $DB_NAME ``` -Each entry: `command` (**required** — the only field the schema requires), `name` (optional, distinguishes processes in logs), `workingDir` (optional), `initCommands` (optional, per-process init). **DO NOT** use both `start` and `startCommands`. +Each entry: `command` (**required** — the only field the schema requires), `name` (optional, distinguishes processes in logs), `workingDir` (optional), `user` (optional, system user the command and its `initCommands` run under, default `zerops`, the user has to exist in the container), `initCommands` (optional, per-process init). **DO NOT** use both `start` and `startCommands`. --- @@ -21231,12 +21468,12 @@ run: values: { X-Frame-Options: "'DENY'" } ``` -- **`cors`**: Sets Access-Control-Allow-Origin. `"*"` auto-converted to `'*'` -- **`redirects[]`**: `from` (wildcards `*`), `to`, `status`, `preservePath`, `preserveQuery` -- **`headers[]`**: `for` (path pattern), `values` (header key-value pairs) -- **`root`**: Custom root directory +- **`cors`**: Sets Access-Control-Allow-Origin, -Allow-Methods, -Allow-Headers and -Expose-Headers to the same value. `"*"` auto-converted to `'*'` +- **`redirects[]`**: `from` (exact path, or prefix with trailing `*`, or absolute URL matched on the domain), `to`, `status` (omit for a masked redirect, `301`/`302` only for absolute `from`), `preservePath`, `preserveQuery` (both only with a `status`) +- **`headers[]`**: `for` (same matching as `from`, `"/*"` for everything, `"/"` is the homepage only), `values` (header key-value pairs, quotes included: `"'DENY'"`). Only the best-matching rule applies, rules are not merged. +- **`root`**: Folder served, relative to `/var/www`. The Static service ignores `run.documentRoot`, this is its replacement. -**DO NOT** use on non-static services -- silently ignored. +**DO NOT** use on non-static services -- silently ignored. Conversely `documentRoot` is ignored on Static, and `siteConfigPath` is ignored on Static whenever `routing` is present. --- @@ -21274,7 +21511,7 @@ Available runtimes and versions are listed in **Service Stacks (live)** -- injec - The OS is part of the base value — `ubuntu/nodejs@22` or `alpine/nodejs@22` — with no separate `os:` field; use the same prefix in `build.base` and `run.base` (a bare `nodejs@22` resolves to Alpine, and `run.base` rewrites the service OS on deploy) - PHP: build `/php@X`, run `/php-nginx@X` or `/php-apache@X` (different bases) - Deno: ships only as `ubuntu/deno@X` (no Alpine image). Gleam runs on both Alpine and Ubuntu. -- Static sites: build `alpine/nodejs@latest`, run `static` +- Static sites: build `alpine/nodejs@latest`, run `alpine/static` (the bare `static` shorthand means the same) - `@latest` = newest stable version ---