Production Server

This guide covers running Accent CMS as a persistent production service. For static site generation, see the Static Site Build guide instead.

The --production Flag

The --production flag switches Accent CMS from development mode to production mode:

accent serve --production

Production mode changes the following behavior:

BehaviorDev ModeProduction Mode
Bind address127.0.0.1 onlyConfigured address (can bind 0.0.0.0)
Dev bannerShownHidden
Hot reloadEnabled (if configured)Disabled
Browser reload SSEActive at /_dev/reloadDisabled
CachingShort TTLs for fast iterationProduction TTLs for performance
LoggingDebug levelInfo level

Production mode requires a valid Standard or Pro license key. Without one, the server exits with an error. All features work without a license in dev mode, so you can evaluate them before purchasing.

License Key Setup

Obtain a license key from accentcms.dev/pricing. Configure it using either method:

In config.yaml

license:
  key: "eyJ..."

As an Environment Variable

export ACCENTCMS_LICENSE="eyJ..."
accent serve --production

The environment variable takes precedence over the config file. This is the recommended approach for containerized deployments, since it keeps the license key out of version control.

Built-in TLS

Accent CMS has built-in TLS support via the --tls flag. In development mode TLS is already the default with an automatically generated self-signed certificate, so the flag only matters in production mode:

accent serve
# Visit https://127.0.0.1:4400

accent serve --production --tls
# Self-signed TLS in production mode as well

For production deployments, provide your own PEM certificate and key (requires a Standard or Pro license):

accent serve --production --tls --cert /etc/ssl/cert.pem --key /etc/ssl/key.pem

Built-in TLS is convenient for simple setups. For advanced needs (load balancing, compression, static asset caching, rate limiting), use a reverse proxy instead.

Reverse Proxy with nginx

In production, place Accent CMS behind a reverse proxy that handles TLS termination, compression, and static asset caching. Here is a minimal nginx configuration:

server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://127.0.0.1:4400;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

For TLS with Let’s Encrypt, add a listen 443 ssl block and use certbot --nginx to provision certificates automatically.

Caching Static Assets

Theme assets (CSS, JS, images) do not change between deployments. Serve them directly from nginx to reduce load on Accent CMS:

location /assets/ {
    alias /opt/accent/my-site/site/themes/default/assets/;
    expires 30d;
    add_header Cache-Control "public, immutable";
}

Reverse Proxy with Caddy

Caddy provides automatic HTTPS with no additional configuration:

example.com {
    reverse_proxy localhost:4400
}

Caddy obtains and renews TLS certificates from Let’s Encrypt automatically. No extra steps are needed.

Reverse proxy with IIS

On Windows Server, IIS can forward requests to Accent CMS with two IIS extensions, URL Rewrite and Application Request Routing (ARR):

  1. Install both extensions.
  2. In IIS Manager, select the server, open Application Request Routing, and select Enable Proxy.
  3. Add a rewrite rule under <system.webServer> in the site’s web.config:
<rewrite>
  <rules>
    <rule name="Accent CMS" stopProcessing="true">
      <match url="(.*)" />
      <action type="Rewrite" url="http://127.0.0.1:4400/{R:1}" />
    </rule>
  </rules>
</rewrite>

Microsoft’s Reverse Proxy with URL Rewrite v2 and Application Request Routing walks through the same steps with screenshots. Caddy, above, also runs on Windows.

Behind any reverse proxy, Accent CMS sees the proxy’s address as the client’s. With protection.ip_allowlist.trust_forwarded_for: true, the IP allowlist reads the first address in the X-Forwarded-For header instead, and that address must have no port. A client can send that header itself, so turn the setting on only when your proxy replaces any X-Forwarded-For header the client sent, rather than adding to it.

Windows Firewall

Behind a reverse proxy, keep Accent CMS on 127.0.0.1 and open only the proxy’s ports. To serve Accent CMS directly, allow its port from an Administrator PowerShell:

New-NetFirewallRule -DisplayName "Accent CMS" -Direction Inbound -Protocol TCP -LocalPort 4400 -Action Allow

Running as a service

Start Accent CMS when the machine starts, and restart it after a failure.

Create a systemd service unit:

[Unit]
Description=Accent CMS
After=network.target

[Service]
Type=simple
User=accent
WorkingDirectory=/opt/accent/my-site
ExecStart=/usr/local/bin/accent serve --production
Restart=on-failure
RestartSec=5
Environment=ACCENTCMS_LICENSE=eyJ...

[Install]
WantedBy=multi-user.target

Install and enable the service:

sudo cp accent.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now accent

Check status and logs:

sudo systemctl status accent
sudo journalctl -u accent -f

Run Accent CMS as a Windows service with accent service. Windows starts the service at boot, restarts it after a failure, and stops it cleanly at shutdown.

Put accent.exe where the service can read it, such as C:\Program Files\Accent\. A service cannot run an accent.exe inside a user profile, which is where the install script puts it.

A service does not see the variables set in your shell, and any local user can read the variables you pass to it with --env. Put the license key and the admin token in config.yaml instead:

license:
  key: eyJ...
admin:
  token: a-long-random-string

From an Administrator PowerShell in the site’s directory, check the site and install the service. install refuses to register a service that would not start, and prints every problem it finds.

Set-Location C:\Sites\my-site
& 'C:\Program Files\Accent\accent.exe' validate --service-check --production
& 'C:\Program Files\Accent\accent.exe' service install -- --config config.yaml --production

Now that the service’s account exists, let only administrators and the service read config.yaml, then start the service:

icacls config.yaml /grant:r "Administrators:F" "SYSTEM:F" "NT SERVICE\accent:R"
icacls config.yaml /inheritancelevel:r
Start-Service accent

Check the status and follow the latest log:

Get-Service accent
Get-ChildItem C:\ProgramData\accent\accent\logs\*.log | Sort-Object LastWriteTime | Select-Object -Last 1 | Get-Content -Wait

To upgrade, stop the service, replace accent.exe, and start the service again: Windows does not let you replace a running program. To remove the service, run accent service uninstall. accent service lists every option.

Stopping the server

accent serve stops gracefully on SIGTERM, which systemctl stop sends, and on Ctrl+C: it stops accepting connections and lets in-flight requests finish. Over TLS it waits at most 5 seconds; over plain HTTP it waits for every request, so set TimeoutStopSec to bound a slow client. accent serve-static and accent docs serve stop the same way.

On Windows, Ctrl+Break and closing the console window also stop the server gracefully. After a console close it stops waiting after 4 seconds, because Windows ends the process 5 seconds after the window closes.

A Windows service stops the same way when an administrator runs Stop-Service accent, or when Windows shuts down. It waits at most 10 seconds, so that it stops in the time Windows allows.

Runtime Config Reload

Accent CMS supports reloading config.yaml without restarting the process. Send SIGHUP to the running service:

sudo systemctl reload accent

Add a ExecReload directive to the unit file to enable this:

ExecReload=/bin/kill -HUP $MAINPID

This allows you to change site settings, switch themes, or toggle features without downtime.

A Windows service has no SIGHUP. From an Administrator PowerShell, reload its configuration with this command, which needs no admin token:

sc.exe control accent paramchange

A reload cannot turn the server back into a development server. --production is a command-line flag, not a config setting, so a reload re-applies its effect to the freshly read file: dev.hot_reload, dev.browser_reload, dev.debug, and dev.show_drafts are forced off whatever config.yaml says. To run with development features, restart without --production.

The same goes for the path and theme flags. --theme, --theme-dir, --content-dir, and --site-dir are re-applied to the freshly read file on every reload, so a server started with --theme dark keeps serving dark even if config.yaml names a different one. (--address and --port are not: the listener is already bound, so changing them needs a restart.)

Two of the dev flags need no mistake on your part to appear: hot_reload and browser_reload default to on, so a config.yaml that never mentions the dev: section still asks for them at every reload. They are clamped silently. The other two default to off, so finding debug: true or show_drafts: true in a reloaded production config means someone wrote it — the server logs a warning naming them, because ignoring an explicit request to expose the debug parameter or unpublished pages is worth telling you about.

Docker Deployment

Dockerfile

FROM debian:bookworm-slim

RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \
    && rm -rf /var/lib/apt/lists/*

COPY accent /usr/local/bin/
COPY my-site/ /app/

WORKDIR /app
EXPOSE 4400

CMD ["accent", "serve", "--production", "--address", "0.0.0.0"]

Build and Run

docker build -t my-site .
docker run -d \
  --name accent \
  -p 4400:4400 \
  -e ACCENTCMS_LICENSE="eyJ..." \
  my-site

Docker Compose

services:
  accent:
    build: .
    ports:
      - "4400:4400"
    environment:
      ACCENTCMS_LICENSE: "${ACCENTCMS_LICENSE}"
    volumes:
      - ./site/content:/app/site/content
    restart: unless-stopped

Mounting the content directory as a volume lets you update content without rebuilding the image.

Environment Variables

These environment variables affect production behavior:

VariableDescriptionDefault
ACCENTCMS_LICENSELicense key for production modeNone (required)
ACCENTCMS_SMTP_PASSWORDSMTP password referenced from config.yamlNone
RUST_LOGLog level (info, debug, trace)info in production

Any value in config.yaml may reference an environment variable as ${VAR} (or ${VAR:-default}), expanded when the config loads. A value that is exactly one placeholder takes the type the environment supplies, so numeric fields (smtp.port: "${SMTP_PORT}") and booleans can be environment-driven too. Only the placeholders themselves are rewritten: every other value reaches the server exactly as written, so a password such as 01234 keeps its leading zero. That holds under every key, including names such as og.type. The one exception is a placeholder inside an anchored value (&name reused through *name): it is expanded on the parsed tree instead, which rewrites number-like strings, and the server logs a warning naming it. ACCENTCMS_LICENSE and ACCENTCMS_SMTP_PASSWORD are the recognised secrets. To override the config file path, bind address, or port, use the --config, --address, and --port CLI flags instead.

Performance Tuning

Cache Size

The page cache stores rendered HTML in memory. Configure the maximum size:

cache:
  max_size_mb: 100
  ttl_seconds: 7200

Each cache entry is small (the rendered HTML plus metadata). For most sites, the default 50MB is sufficient.

Cache Warming

By default, the first request to each page pays the full rendering cost (up to ~1s for pages with many syntax-highlighted code blocks). Enable cache warming to pre-render all pages at startup:

cache:
  warming: true

The server remains available immediately – warming runs as a background task. Once complete, every page is a cache hit from the first request. Recommended for production deployments where consistent response times matter.

HTTP Cache Headers

In production, Accent CMS automatically sets Cache-Control, ETag, and Vary headers on all responses based on content type. Browsers and reverse proxies use these headers to cache responses and avoid unnecessary re-downloads.

The defaults are optimized for typical CMS usage:

  • HTML pages: 5 min browser cache, 1 hour shared cache
  • CSS/JS/WASM: 1 year immutable (safe because assets change on theme reload)
  • Images/media: 24 hours
  • Fonts: 1 year immutable
  • RSS feeds: 1 hour

Override per content type or per route in config.yaml:

http_headers:
  cache:
    defaults:
      pages: "public, max-age=600, s-maxage=7200"
    routes:
      - path: "/api/*"
        cache_control: "no-store"

Conditional requests (If-None-Match, If-Modified-Since) are handled automatically, returning 304 Not Modified when the client’s cached version is still valid.

CORS

Cross-origin resource sharing is disabled by default. Enable it when fonts, media, or API endpoints are served from a different origin or CDN subdomain:

http_headers:
  cors:
    enabled: true
    allowed_origins:
      - "https://cdn.example.com"
    routes:
      - path: "/api/*"
        allowed_origins: ["*"]
        allowed_methods: ["GET", "HEAD", "POST", "OPTIONS"]

When http_headers.cors is configured, it supersedes the legacy api.cors toggle.

Connection Limits

When running behind a reverse proxy, the proxy handles connection management. If exposing Accent CMS directly, consider OS-level tuning:

# Increase file descriptor limit for the accent user
echo "accent soft nofile 65535" >> /etc/security/limits.conf

Health Checks

Accent CMS exposes a health endpoint for load balancers and monitoring:

GET /health

Response:

{
  "status": "ok",
  "version": "0.1.0",
  "uptime_seconds": 3600
}

Configure your load balancer to poll this endpoint:

# nginx upstream health check (requires nginx-plus or third-party module)
upstream accent {
    server 127.0.0.1:4400;
}

For Docker, add a health check to your Compose file:

services:
  accent:
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:4400/health"]
      interval: 30s
      timeout: 5s
      retries: 3

Next Steps