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#
- 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.tomltolisten = [":8080"]and runsudo systemctl restart arka. - Write one host file per site (or create them in the panel) with
listen = [":8080"]for now. Use the tables below. - Test each site against Arka without touching DNS — see the command below.
- Switch over in a quiet moment: stop the old server (
sudo systemctl disable --now nginx), change the hosts to the normal ports (remove thelistenlines, add[tls] acme = true), and restart Arka. Arka requests certificates for every site within a minute. - 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
httptoservertolocation. In Arka each host file is complete on its own; share settings on purpose with snippets. - No
if. Conditions are explicit:whenon 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.