Skip to content

Static Sites

Host static websites, landing pages, and single-page applications through Portlama without a tunnel.

In Plain English

Not everything needs a tunnel. If you have a static website — a marketing page, a blog built with a static site generator, or a single-page application — you can host it directly on your Portlama server. Upload your files through the panel, and they are served via nginx with a Let's Encrypt certificate. No local machine needed, no Chisel client required.

Static sites come in two flavors: managed subdomains (like blog.example.com) where Portlama handles everything, and custom domains (like myblog.com) where you bring your own domain and point it at the server.

Prerequisites

  • A completed Portlama onboarding
  • Access to the Portlama admin panel
  • Static files to upload (HTML, CSS, JS, images, etc.)
  • For custom domains: access to DNS management for the custom domain

Step-by-Step

1. Open the Sites Page

Log in to the Portlama admin panel at https://panel.example.com (or https://<ip>:9292).

Click Sites in the sidebar navigation (if available) or navigate to the Sites section.

2. Create a Managed Subdomain Site

A managed subdomain site uses your Portlama domain (e.g., blog.example.com). Certificate issuance, DNS, and nginx configuration are handled automatically.

  1. Click Add Site.
  2. Select Managed Subdomain as the domain type.
  3. Enter a name (this becomes the subdomain):
FieldRulesExample
NameLowercase alphanumeric with hyphens, max 100 charsblog

The name must not conflict with existing tunnels or reserved subdomains (panel, auth, tunnel, www, mail, ftp, api).

  1. Configure options:
OptionDefaultDescription
SPA ModeOffIf enabled, all non-file requests return index.html (for React, Vue, Angular apps)
Authelia ProtectedOffIf enabled, visitors must log in through Authelia before accessing the site
  1. Click Create Site.

What happens:

  1. A Let's Encrypt certificate is issued for blog.example.com.
  2. An nginx vhost is written to serve static files from /var/www/portlama/<site-id>/.
  3. The site directory is created.
  4. The site record is saved to /etc/portlama/sites.json.

The site appears in the list with a "Live" status badge. It is ready for file uploads.

3. Create a Custom Domain Site

A custom domain site uses a domain you own (e.g., myblog.com). You must configure DNS before the site goes live.

  1. Click Add Site.
  2. Select Custom Domain as the domain type.
  3. Enter a name and the custom domain:
FieldExample
Namemyblog
Custom Domainmyblog.com
  1. Configure SPA Mode and Authelia Protection as needed.
  2. Click Create Site.

The site is created with a "DNS Pending" status. No certificate is issued yet because Let's Encrypt needs the domain to resolve to your server first.

4. Verify DNS for a Custom Domain

  1. Add an A record at your domain registrar:
TypeNameValue
Amyblog.com (or @)Your server IP (e.g., 203.0.113.42)
  1. Wait for DNS propagation (usually a few minutes).
  2. In the Portlama panel, click Verify DNS next to the site.
  3. The panel checks if the domain resolves to your server.

If DNS is verified:

  • A Let's Encrypt certificate is issued automatically.
  • An nginx vhost is configured.
  • The site status changes to "Live".

If DNS is not verified:

  • The panel shows the current resolution and expected IP.
  • Wait longer and try again.

5. Upload Files

After creating a site (whether managed or custom with verified DNS), upload your static files:

  1. Click the Files button next to the site.
  2. The file browser opens, showing the site's root directory.
  3. Click Upload to select and upload files.
  4. Files are uploaded to the site directory and served immediately.

File upload details:

  • Files are uploaded via multipart form data to POST /api/sites/:id/files.
  • The upload target directory can be specified with the ?path= query parameter.
  • Files are served directly by nginx — no Node.js involvement in serving static content.
  • The site's total size is tracked and displayed.

Directory structure example:

For a typical static site, upload:

index.html
styles.css
scripts.js
images/
  logo.png
  hero.jpg

6. Enable SPA Mode

If your site is a single-page application (React, Vue, Angular, Svelte, etc.), enable SPA mode:

  1. When creating the site, check the SPA Mode checkbox.
  2. Or edit the site configuration after creation.

With SPA mode enabled, nginx is configured with a try_files directive: any request that does not match a physical file is served index.html. This allows client-side routing to work correctly.

Without SPA mode: A request to blog.example.com/about looks for a file at /about or /about/index.html. If neither exists, nginx returns 404.

With SPA mode: A request to blog.example.com/about first checks for a file at /about. If no file exists, it serves index.html, and your JavaScript router handles the route.

7. Enable Authelia Protection

If you want visitors to log in before accessing your static site:

  1. When creating the site, check the Authelia Protected checkbox.
  2. Or edit the site configuration after creation.

With Authelia protection enabled, nginx adds an auth_request directive. Every request is checked against Authelia before serving the file. Visitors are redirected to auth.example.com to log in with username, password, and TOTP code.

This is useful for internal documentation, staging sites, or any content you want to restrict to authenticated users.

8. Manage Files

The file browser lets you:

  • Browse directories — Navigate the file tree
  • Upload files — Add new files to any directory
  • Delete files — Remove individual files
  • View file sizes — See the total site size

9. Delete a Site

  1. Go to the Sites page.
  2. Click the Delete button next to the site.
  3. Confirm the deletion.

What is removed:

  • The nginx vhost configuration
  • All uploaded files in the site directory
  • The site record from sites.json

The Let's Encrypt certificate is left in place (certbot manages its lifecycle).

For Developers

Site Data Model

Each site is stored in /etc/portlama/sites.json as a JSON array:

json
[
  {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "blog",
    "fqdn": "blog.example.com",
    "type": "managed",
    "spaMode": false,
    "autheliaProtected": false,
    "allowedUsers": [],
    "dnsVerified": true,
    "certIssued": true,
    "rootPath": "/var/www/portlama/550e8400-e29b-41d4-a716-446655440000",
    "createdAt": "2024-01-15T10:30:00.000Z",
    "totalSize": 245760
  }
]

API Endpoints

MethodPathPurpose
GET/api/sitesList all sites
POST/api/sitesCreate a new site
PATCH/api/sites/:idUpdate site settings (spaMode, autheliaProtected, allowedUsers)
DELETE/api/sites/:idDelete a site
POST/api/sites/:id/verify-dnsVerify DNS for custom domain sites
GET/api/sites/:id/filesList files in a directory
POST/api/sites/:id/filesUpload files (multipart)
DELETE/api/sites/:id/filesDelete a file

Create Site Request

Managed subdomain:

json
POST /api/sites
{
  "name": "blog",
  "type": "managed",
  "spaMode": false,
  "autheliaProtected": false
}

Custom domain:

json
POST /api/sites
{
  "name": "myblog",
  "type": "custom",
  "customDomain": "myblog.com",
  "spaMode": true,
  "autheliaProtected": false
}

Validation Rules

FieldRules
name1-100 chars, regex ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$, unique across sites, not reserved, not colliding with tunnels
typemanaged or custom
customDomainRequired for custom type, max 253 chars, valid domain format
spaModeBoolean, defaults to false
autheliaProtectedBoolean, defaults to false
allowedUsersArray of strings (Authelia usernames), defaults to []

File Storage

Static files are stored under /var/www/portlama/<site-id>/. The directory is owned by www-data:www-data and served directly by nginx.

File operations use sudo commands with restricted paths:

  • mkdir -p /var/www/portlama/<id>/
  • chown -R www-data:www-data /var/www/portlama/<id>/
  • rm -rf /var/www/portlama/<id>/

Path validation prevents directory traversal attacks. The validatePath() function rejects paths containing .., absolute paths, and paths outside the site root.

Managed vs. Custom Domain Flow

Managed subdomain:

Create → Issue cert → Write vhost → Create dir → Save state → Live

Everything happens in one request. The site is immediately live after creation (assuming wildcard DNS is configured, or the subdomain has its own A record).

Custom domain:

Create → Create dir → Save state → DNS Pending

Verify DNS → Issue cert → Write vhost → Save state → Live

The site is created in a pending state. DNS verification is a separate step that triggers certificate issuance and vhost creation.

nginx Vhost Features

Each static site vhost includes:

  • TLS termination with Let's Encrypt certificate
  • Optional try_files $uri $uri/ /index.html for SPA mode
  • Optional Authelia auth_request for protected sites
  • Standard security headers
  • Gzip compression for text-based assets

CLI Deployment

As an alternative to the browser-based file management described above, you can manage static sites and deploy files directly from the command line using the Portlama agent CLI.

Prerequisites

  • The Portlama agent CLI installed on your machine (see Mac Client Setup)
  • An agent certificate with sites:read and sites:write capabilities. Generate one from the panel: Certificates > Agent Certificates > Generate, and check the sites:read and sites:write capability boxes.
  • The agent certificate must have the target site listed in its Site Access configuration. The admin assigns sites to agents from Panel > Certificates > edit agent > Site Access.

Workflow

Site creation and deletion are admin-only operations. The admin creates the site through the panel or admin certificate, then assigns it to the agent. The agent can then deploy files to the site.

bash
# Admin creates the site through the panel UI first, then:

# Build your app and deploy (agent cert must have the site in its allowedSites)
npm run build
portlama-agent deploy blog ./dist

The deploy command clears all existing files on the site and uploads all non-hidden files from the specified local directory. It is a full replacement, not a merge. Every deploy is a clean slate.

You can also list sites assigned to your agent from the CLI:

bash
# List sites assigned to this agent
portlama-agent sites

For the full list of flags and options, see the agent CLI README.

Quick Reference

FeatureManaged SubdomainCustom Domain
DNS setupAutomatic (wildcard)Manual (A record)
CertificateAutomaticAfter DNS verification
Goes liveImmediatelyAfter DNS + cert
Example domainblog.example.commyblog.com
ActionSteps
Create managed siteAdd Site, Managed Subdomain, enter name, Create
Create custom siteAdd Site, Custom Domain, enter name + domain, Create
Verify DNSClick "Verify DNS" next to pending site
Upload filesClick "Files", then "Upload"
Enable SPA modeCheck "SPA Mode" when creating
Enable authCheck "Authelia Protected" when creating
Delete siteClick "Delete", confirm
Reserved NamesCannot Use As Site Name
panelAdmin panel subdomain
authAuthelia subdomain
tunnelChisel tunnel subdomain
wwwCommon web prefix
mailEmail subdomain
ftpFile transfer subdomain
apiAPI subdomain

Released under the PolyForm Noncommercial License 1.0.0