Moving from nginx or Apache

Most nginx and Apache features have a direct equivalent in Arka, usually shorter. This page gives a safe order of work and translation tables for the directives people use most.

A safe plan#

  1. Install Arka next to the old server, so nothing changes for visitors yet. Arka's welcome site wants port 80, which the old server still holds: change /etc/arka/hosts/welcome.toml to listen = [":8080"] and run sudo systemctl restart arka.
  2. Write one host file per site (or create them in the panel) with listen = [":8080"] for now. Use the tables below.
  3. Test each site against Arka without touching DNS — see the command below.
  4. Switch over in a quiet moment: stop the old server (sudo systemctl disable --now nginx), change the hosts to the normal ports (remove the listen lines, add [tls] acme = true), and restart Arka. Arka requests certificates for every site within a minute.
  5. Keep the old server installed but stopped for a while. Going back is sudo systemctl stop arka && sudo systemctl start nginx.

Testing a site on Arka while DNS still points at the old server:

curl -sv --resolve example.com:8080:127.0.0.1 http://example.com:8080/

Certificates. Let Arka get its own (acme = true) — it needs port 80 for a minute. Reusing certbot's files in /etc/letsencrypt also works (cert and key under [tls]), but the arka user must be able to read the private key, and certbot's renewals then need systemctl reload arka as a deploy hook.

.htaccess files are not read by Arka. Move their rules into the host file: most WordPress, Laravel and Drupal .htaccess files only send unknown paths to index.php, which is front_controller = "/index.php".

A worked example#

An nginx site with a PHP application, an API behind a proxy and the usual HTTPS redirect:

server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}
server {
    listen 443 ssl http2;
    server_name example.com www.example.com;
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    root /var/www/example/public;
    client_max_body_size 32m;
    add_header X-Frame-Options SAMEORIGIN;

    location /api/ {
        proxy_pass http://127.0.0.1:3000/;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }
    location ~ /\.(?!well-known) { deny all; }
}

The same site in Arka — one file, and the HTTPS redirect, forwarded headers and hidden dotfiles come built in:

# /etc/arka/hosts/example.com.toml
server_names = ["example.com", "www.example.com"]

[tls]
acme = true
redirect_http = true

[limits]
max_body_size = "32MB"

[headers]
security = true            # X-Frame-Options and the other recommended headers

[[routes]]
path = "/api/"
handler = { type = "proxy", upstreams = ["127.0.0.1:3000"], strip_prefix = true, preserve_host = true }

[[routes]]
path = "/"
handler = { type = "fastcgi", address = "unix:/run/php/php8.3-fpm.sock", root = "/var/www/example/public", front_controller = "/index.php", max_conns = 5 }

From nginx#

nginx Arka
server { … } one file in /etc/arka/hosts/
server_name a.com *.a.com; server_names = ["a.com", "*.a.com"] (also ~regex and www.a.*)
listen 80; listen 443 ssl; listen = [":80"], [tls] listen = [":443"] (both are the defaults)
default_server default = true
ssl_certificate, ssl_certificate_key [tls] cert, key — or acme = true instead of certbot
return 301 https://$host$request_uri; [tls] redirect_http = true
add_header Strict-Transport-Security … [tls] hsts = true
ssl_protocols, ssl_ciphers, ssl_stapling min_version, ciphers (IANA names), ocsp_stapling (on)
ssl_client_certificate, ssl_verify_client [tls] client_auth = { ca = … }
location /x/ { … } [[routes]] path = "/x/"
location = /x / location ~ re match = "exact" / match = "regex"
root handler = { type = "static", root = … }
alias strip_prefix = true with root
try_files $uri $uri/ /index.html; try_files = ["{path}", "{path}/", "/index.html"], or fallback = "index.html"
autoindex on; browse = true
index index = ["index.html"]
expires 7d; / add_header Cache-Control cache_control = "public, max-age=604800"
gzip on; gzip_static on; [compression] (on by default: brotli, zstd, gzip) and precompressed (on)
proxy_pass http://127.0.0.1:3000/; handler = { type = "proxy", upstreams = ["127.0.0.1:3000"], strip_prefix = true }
proxy_set_header Host $host; preserve_host = true
proxy_set_header X-Forwarded-*, X-Real-IP sent automatically, and cannot be forged by visitors
upstream { server a weight=2; server b backup; } upstreams = ["a weight=2", "b backup"]
least_conn; ip_hash; hash $x; strategy = "least_conn", "ip_hash", "hash" with hash_key
keepalive 32; (upstream) always on
max_fails=3 fail_timeout=10s max_fails, fail_timeout
proxy_next_upstream error timeout http_502; retry_on = ["error", "timeout", "502"]
proxy_connect_timeout, proxy_read_timeout connect_timeout, read_timeout
proxy_buffering, proxy_request_buffering buffering = {}
proxy_cache_path, proxy_cache, proxy_cache_use_stale cache = { max_size, disk, stale_if_error, stale_while_revalidate }
proxy_cache_lock on; lock (on by default)
proxy_redirect, proxy_cookie_domain rewrite_redirects (on), redirects, cookies
proxy_hide_header response_headers = { remove = [ … ] }
proxy_intercept_errors on; intercept_errors = true
proxy_http_version 1.1; + Upgrade headers WebSockets work by default
grpc_pass http2 = true on a proxy route
fastcgi_pass + fastcgi_params handler = { type = "fastcgi", address = …, root = … }
try_files … /index.php?$args; front_controller = "/index.php"
client_max_body_size [limits] max_body_size
client_body_timeout, send_timeout [limits] body_timeout, [server] write_timeout
rewrite ^/a/(.*)$ /b/$1 last; [[rewrites]] from = "^/a/(.*)$", to = "/b/{1}"
rewrite … permanent; / return 301 …; handler = { type = "redirect", to = …, code = 301 }
return 200 "ok"; handler = { type = "respond", body = "ok" }
add_header X-A b; [headers] set = { "X-A" = "b" }
error_page 404 /404.html; [error_pages] 404 = "/var/www/errors/404.html"
access_log, log_format [log] path, format, template
limit_req zone=… burst=… [rate_limit] requests_per_second, burst
limit_conn [connections] per_client
limit_rate [bandwidth] rate
allow, deny [access] allow, deny
auth_basic + auth_basic_user_file auth = { type = "basic", users_file = … }
auth_request auth = { type = "forward", url = … }
secure_link signed = { secret = … }
internal; internal = true
X-Accel-Redirect from the app accel_redirect = true
map $a $b { … } [[variables]]
sub_filter body_replace
ssi on; ssi = true
mp4; mp4 = true
set_real_ip_from, real_ip_header [real_ip] from, header
geoip2 module + if ($country …) [geoip] and [geo], or when conditions on routes
stream { … } files in /etc/arka/streams/
ModSecurity + OWASP CRS [waf] level = "standard" (paranoia 1; "strict" and "maximum" for 2 and 3) — over-limit bodies are refused like SecRequestBodyLimitAction Reject
nginx -t arka check
nginx -s reload nothing — saving the file applies it (or systemctl reload arka)

From Apache#

Apache Arka
<VirtualHost *:80> one file in /etc/arka/hosts/
ServerName, ServerAlias server_names
DocumentRoot handler = { type = "static", root = … }
DirectoryIndex index
Options Indexes browse = true
Alias /x /path a route path = "/x/" with root = "/path", strip_prefix = true
Redirect permanent /old https://… a redirect route, code = 301
RewriteEngine + RewriteRule [[rewrites]], or a redirect route
.htaccess front-controller rules front_controller = "/index.php"
RewriteCond %{HTTPS} off → https [tls] redirect_http = true
SSLEngine, SSLCertificateFile [tls] cert, key — or acme = true
mod_php, or php-fpm through SetHandler proxy:unix:… handler = { type = "fastcgi", address = "unix:/run/php/php8.3-fpm.sock", … } (Arka uses php-fpm)
ProxyPass, ProxyPassReverse a proxy route; rewrite_redirects is on by default
ProxyPreserveHost On preserve_host = true
<Proxy balancer://…> several upstreams and a strategy
mod_deflate, mod_brotli [compression] (on by default)
mod_expires, Header set Cache-Control cache_control on the route
Header set, Header unset [headers] set, remove
Require ip 10.0.0.0/8 [access] allow = ["10.0.0.0/8"]
AuthType Basic, AuthUserFile auth = { type = "basic", users_file = … } (htpasswd files work as they are)
ErrorDocument 404 /404.html [error_pages]
LimitRequestBody [limits] max_body_size
mod_security [waf] level = "standard" (the OWASP Core Rule Set)
mod_evasive, fail2ban [rate_limit] and [bans]
mod_remoteip [real_ip]
mod_include (SSI) ssi = true
mod_dav handler = { type = "webdav", … }
CustomLog, LogFormat [log]
apachectl configtest arka check
systemctl reload apache2 nothing — saving the file applies it

What is different#

  • No inheritance. nginx and Apache settings flow from http to server to location. In Arka each host file is complete on its own; share settings on purpose with snippets.
  • No if. Conditions are explicit: when on routes, [[variables]] for computed values, [access] and [geo] for addresses and countries.
  • Saving applies. There is no reload step — and a broken file never takes a site down, because its last good version keeps serving.
  • Safe defaults. Forwarded headers that visitors cannot forge, retries that never repeat a POST, dotfiles hidden, symbolic links kept inside the site, compression and caching of static files — all without configuration.