[{"data":1,"prerenderedAt":1326},["ShallowReactive",2],{"content-developer\u002Fself-hosting-production":3,"surround-\u002Fdeveloper\u002Fself-hosting-production":1317},{"id":4,"title":5,"body":6,"description":1310,"extension":1311,"meta":1312,"navigation":223,"path":1313,"seo":1314,"stem":1315,"__hash__":1316},"content\u002F3.developer\u002F33.self-hosting-production.md","Production Deployment",{"type":7,"value":8,"toc":1291},"minimark",[9,19,24,27,32,35,46,53,99,102,180,184,321,324,356,360,363,456,469,473,476,486,493,520,526,550,554,561,623,632,671,674,691,695,699,706,712,721,727,848,852,859,918,922,925,929,933,936,950,953,1170,1174,1177,1189,1192,1196,1276,1287],[10,11,12,13,18],"p",{},"This guide covers hardening your self-hosted Owlat instance for production use. For initial setup, see ",[14,15,17],"a",{"href":16},"\u002Fdeveloper\u002Fself-hosting","Self-Hosting",".",[20,21,23],"h2",{"id":22},"reverse-proxy","Reverse Proxy",[10,25,26],{},"In production, place a reverse proxy in front of the Docker stack to handle TLS termination and route traffic.",[28,29,31],"h3",{"id":30},"caddy-recommended","Caddy (Recommended)",[10,33,34],{},"Caddy automatically provisions and renews TLS certificates via Let's Encrypt.",[36,37,42],"pre",{"className":38,"code":40,"language":41},[39],"language-text","# Caddyfile\nowlat.example.com {\n    # Web application\n    reverse_proxy localhost:3000\n}\n\nconvex.example.com {\n    # Convex backend API\n    reverse_proxy localhost:3210\n}\n\nconvex-site.example.com {\n    # Convex HTTP actions (tracking pixels, webhooks, auth)\n    reverse_proxy localhost:3211\n}\n","text",[43,44,40],"code",{"__ignoreMap":45},"",[10,47,48,49,52],{},"After setting up Caddy, update your ",[43,50,51],{},".env"," to use the HTTPS URLs:",[36,54,58],{"className":55,"code":56,"language":57,"meta":45,"style":45},"language-bash shiki shiki-themes github-light github-dark-dimmed","NUXT_PUBLIC_CONVEX_URL=https:\u002F\u002Fconvex.example.com\nNUXT_PUBLIC_CONVEX_SITE_URL=https:\u002F\u002Fconvex-site.example.com\nNUXT_PUBLIC_SITE_URL=https:\u002F\u002Fowlat.example.com\n","bash",[43,59,60,77,88],{"__ignoreMap":45},[61,62,65,69,73],"span",{"class":63,"line":64},"line",1,[61,66,68],{"class":67},"sYgZi","NUXT_PUBLIC_CONVEX_URL",[61,70,72],{"class":71},"s7YZ4","=",[61,74,76],{"class":75},"s-HuK","https:\u002F\u002Fconvex.example.com\n",[61,78,80,83,85],{"class":63,"line":79},2,[61,81,82],{"class":67},"NUXT_PUBLIC_CONVEX_SITE_URL",[61,84,72],{"class":71},[61,86,87],{"class":75},"https:\u002F\u002Fconvex-site.example.com\n",[61,89,91,94,96],{"class":63,"line":90},3,[61,92,93],{"class":67},"NUXT_PUBLIC_SITE_URL",[61,95,72],{"class":71},[61,97,98],{"class":75},"https:\u002F\u002Fowlat.example.com\n",[10,100,101],{},"Then update the matching Convex environment variables:",[36,103,105],{"className":55,"code":104,"language":57,"meta":45,"style":45},"npx convex env set SITE_URL \"https:\u002F\u002Fowlat.example.com\" --url http:\u002F\u002Flocalhost:3210 --admin-key \u003Ckey>\nnpx convex env set CONVEX_SITE_URL \"https:\u002F\u002Fconvex-site.example.com\" --url http:\u002F\u002Flocalhost:3210 --admin-key \u003Ckey>\n",[43,106,107,150],{"__ignoreMap":45},[61,108,109,113,116,119,122,125,128,132,135,138,141,144,147],{"class":63,"line":64},[61,110,112],{"class":111},"sOLd2","npx",[61,114,115],{"class":75}," convex",[61,117,118],{"class":75}," env",[61,120,121],{"class":75}," set",[61,123,124],{"class":75}," SITE_URL",[61,126,127],{"class":75}," \"https:\u002F\u002Fowlat.example.com\"",[61,129,131],{"class":130},"sviXB"," --url",[61,133,134],{"class":75}," http:\u002F\u002Flocalhost:3210",[61,136,137],{"class":130}," --admin-key",[61,139,140],{"class":71}," \u003C",[61,142,143],{"class":75},"ke",[61,145,146],{"class":67},"y",[61,148,149],{"class":71},">\n",[61,151,152,154,156,158,160,163,166,168,170,172,174,176,178],{"class":63,"line":79},[61,153,112],{"class":111},[61,155,115],{"class":75},[61,157,118],{"class":75},[61,159,121],{"class":75},[61,161,162],{"class":75}," CONVEX_SITE_URL",[61,164,165],{"class":75}," \"https:\u002F\u002Fconvex-site.example.com\"",[61,167,131],{"class":130},[61,169,134],{"class":75},[61,171,137],{"class":130},[61,173,140],{"class":71},[61,175,143],{"class":75},[61,177,146],{"class":67},[61,179,149],{"class":71},[28,181,183],{"id":182},"nginx-certbot","Nginx + Certbot",[36,185,189],{"className":186,"code":187,"language":188,"meta":45,"style":45},"language-nginx shiki shiki-themes github-light github-dark-dimmed","server {\n    listen 80;\n    server_name owlat.example.com;\n    return 301 https:\u002F\u002F$server_name$request_uri;\n}\n\nserver {\n    listen 443 ssl http2;\n    server_name owlat.example.com;\n\n    ssl_certificate \u002Fetc\u002Fletsencrypt\u002Flive\u002Fowlat.example.com\u002Ffullchain.pem;\n    ssl_certificate_key \u002Fetc\u002Fletsencrypt\u002Flive\u002Fowlat.example.com\u002Fprivkey.pem;\n\n    location \u002F {\n        proxy_pass http:\u002F\u002Flocalhost:3000;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Proto $scheme;\n    }\n}\n\n# Repeat similar blocks for convex.example.com (:3210) and convex-site.example.com (:3211)\n","nginx",[43,190,191,196,201,206,212,218,225,230,236,241,246,252,258,263,269,275,281,287,293,299,305,310,315],{"__ignoreMap":45},[61,192,193],{"class":63,"line":64},[61,194,195],{},"server {\n",[61,197,198],{"class":63,"line":79},[61,199,200],{},"    listen 80;\n",[61,202,203],{"class":63,"line":90},[61,204,205],{},"    server_name owlat.example.com;\n",[61,207,209],{"class":63,"line":208},4,[61,210,211],{},"    return 301 https:\u002F\u002F$server_name$request_uri;\n",[61,213,215],{"class":63,"line":214},5,[61,216,217],{},"}\n",[61,219,221],{"class":63,"line":220},6,[61,222,224],{"emptyLinePlaceholder":223},true,"\n",[61,226,228],{"class":63,"line":227},7,[61,229,195],{},[61,231,233],{"class":63,"line":232},8,[61,234,235],{},"    listen 443 ssl http2;\n",[61,237,239],{"class":63,"line":238},9,[61,240,205],{},[61,242,244],{"class":63,"line":243},10,[61,245,224],{"emptyLinePlaceholder":223},[61,247,249],{"class":63,"line":248},11,[61,250,251],{},"    ssl_certificate \u002Fetc\u002Fletsencrypt\u002Flive\u002Fowlat.example.com\u002Ffullchain.pem;\n",[61,253,255],{"class":63,"line":254},12,[61,256,257],{},"    ssl_certificate_key \u002Fetc\u002Fletsencrypt\u002Flive\u002Fowlat.example.com\u002Fprivkey.pem;\n",[61,259,261],{"class":63,"line":260},13,[61,262,224],{"emptyLinePlaceholder":223},[61,264,266],{"class":63,"line":265},14,[61,267,268],{},"    location \u002F {\n",[61,270,272],{"class":63,"line":271},15,[61,273,274],{},"        proxy_pass http:\u002F\u002Flocalhost:3000;\n",[61,276,278],{"class":63,"line":277},16,[61,279,280],{},"        proxy_set_header Host $host;\n",[61,282,284],{"class":63,"line":283},17,[61,285,286],{},"        proxy_set_header X-Real-IP $remote_addr;\n",[61,288,290],{"class":63,"line":289},18,[61,291,292],{},"        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n",[61,294,296],{"class":63,"line":295},19,[61,297,298],{},"        proxy_set_header X-Forwarded-Proto $scheme;\n",[61,300,302],{"class":63,"line":301},20,[61,303,304],{},"    }\n",[61,306,308],{"class":63,"line":307},21,[61,309,217],{},[61,311,313],{"class":63,"line":312},22,[61,314,224],{"emptyLinePlaceholder":223},[61,316,318],{"class":63,"line":317},23,[61,319,320],{},"# Repeat similar blocks for convex.example.com (:3210) and convex-site.example.com (:3211)\n",[10,322,323],{},"Install certificates with Certbot:",[36,325,327],{"className":55,"code":326,"language":57,"meta":45,"style":45},"sudo certbot --nginx -d owlat.example.com -d convex.example.com -d convex-site.example.com\n",[43,328,329],{"__ignoreMap":45},[61,330,331,334,337,340,343,346,348,351,353],{"class":63,"line":64},[61,332,333],{"class":111},"sudo",[61,335,336],{"class":75}," certbot",[61,338,339],{"class":130}," --nginx",[61,341,342],{"class":130}," -d",[61,344,345],{"class":75}," owlat.example.com",[61,347,342],{"class":130},[61,349,350],{"class":75}," convex.example.com",[61,352,342],{"class":130},[61,354,355],{"class":75}," convex-site.example.com\n",[20,357,359],{"id":358},"firewall","Firewall",[10,361,362],{},"Use UFW (or your distribution's firewall) to restrict access to only the necessary ports.",[36,364,366],{"className":55,"code":365,"language":57,"meta":45,"style":45},"# Allow SSH\nsudo ufw allow 22\u002Ftcp\n\n# Allow HTTP\u002FHTTPS (reverse proxy)\nsudo ufw allow 80\u002Ftcp\nsudo ufw allow 443\u002Ftcp\n\n# Allow SMTP (bounce processing)\nsudo ufw allow 25\u002Ftcp\n\n# Enable the firewall\nsudo ufw enable\n",[43,367,368,374,387,391,396,407,418,422,427,438,442,447],{"__ignoreMap":45},[61,369,370],{"class":63,"line":64},[61,371,373],{"class":372},"sDN9O","# Allow SSH\n",[61,375,376,378,381,384],{"class":63,"line":79},[61,377,333],{"class":111},[61,379,380],{"class":75}," ufw",[61,382,383],{"class":75}," allow",[61,385,386],{"class":75}," 22\u002Ftcp\n",[61,388,389],{"class":63,"line":90},[61,390,224],{"emptyLinePlaceholder":223},[61,392,393],{"class":63,"line":208},[61,394,395],{"class":372},"# Allow HTTP\u002FHTTPS (reverse proxy)\n",[61,397,398,400,402,404],{"class":63,"line":214},[61,399,333],{"class":111},[61,401,380],{"class":75},[61,403,383],{"class":75},[61,405,406],{"class":75}," 80\u002Ftcp\n",[61,408,409,411,413,415],{"class":63,"line":220},[61,410,333],{"class":111},[61,412,380],{"class":75},[61,414,383],{"class":75},[61,416,417],{"class":75}," 443\u002Ftcp\n",[61,419,420],{"class":63,"line":227},[61,421,224],{"emptyLinePlaceholder":223},[61,423,424],{"class":63,"line":232},[61,425,426],{"class":372},"# Allow SMTP (bounce processing)\n",[61,428,429,431,433,435],{"class":63,"line":238},[61,430,333],{"class":111},[61,432,380],{"class":75},[61,434,383],{"class":75},[61,436,437],{"class":75}," 25\u002Ftcp\n",[61,439,440],{"class":63,"line":243},[61,441,224],{"emptyLinePlaceholder":223},[61,443,444],{"class":63,"line":248},[61,445,446],{"class":372},"# Enable the firewall\n",[61,448,449,451,453],{"class":63,"line":254},[61,450,333],{"class":111},[61,452,380],{"class":75},[61,454,455],{"class":75}," enable\n",[457,458,461],"callout",{"title":459,"type":460},"Do not expose internal ports","warning",[10,462,463,464,468],{},"Ports 3210, 3211, 3000, 3100, 6379, and 3310 should ",[465,466,467],"strong",{},"not"," be exposed to the internet. The reverse proxy handles external traffic on ports 80\u002F443 and forwards to internal services.",[20,470,472],{"id":471},"secure-the-dashboard","Secure the Dashboard",[10,474,475],{},"The Convex dashboard on port 6791 provides full read\u002Fwrite access to your database. Never expose it publicly.",[10,477,478,481,482,485],{},[465,479,480],{},"Option 1: Bind to localhost only"," (default in ",[43,483,484],{},"docker-compose.yml",")",[10,487,488,489,492],{},"The dashboard is already bound to ",[43,490,491],{},"127.0.0.1:6791"," by default. Verify with:",[36,494,496],{"className":55,"code":495,"language":57,"meta":45,"style":45},"docker compose port convex-dashboard 6791\n# Should show: 0.0.0.0:6791 or 127.0.0.1:6791\n",[43,497,498,515],{"__ignoreMap":45},[61,499,500,503,506,509,512],{"class":63,"line":64},[61,501,502],{"class":111},"docker",[61,504,505],{"class":75}," compose",[61,507,508],{"class":75}," port",[61,510,511],{"class":75}," convex-dashboard",[61,513,514],{"class":130}," 6791\n",[61,516,517],{"class":63,"line":79},[61,518,519],{"class":372},"# Should show: 0.0.0.0:6791 or 127.0.0.1:6791\n",[10,521,522,525],{},[465,523,524],{},"Option 2: SSH tunnel"," (for remote access)",[36,527,529],{"className":55,"code":528,"language":57,"meta":45,"style":45},"ssh -L 6791:localhost:6791 user@your-server\n# Then open http:\u002F\u002Flocalhost:6791 in your browser\n",[43,530,531,545],{"__ignoreMap":45},[61,532,533,536,539,542],{"class":63,"line":64},[61,534,535],{"class":111},"ssh",[61,537,538],{"class":130}," -L",[61,540,541],{"class":75}," 6791:localhost:6791",[61,543,544],{"class":75}," user@your-server\n",[61,546,547],{"class":63,"line":79},[61,548,549],{"class":372},"# Then open http:\u002F\u002Flocalhost:6791 in your browser\n",[20,551,553],{"id":552},"redis-authentication","Redis Authentication",[10,555,556,557,560],{},"For production, add a password to Redis. Create a ",[43,558,559],{},"docker-compose.override.yml",":",[36,562,566],{"className":563,"code":564,"language":565,"meta":45,"style":45},"language-yaml shiki shiki-themes github-light github-dark-dimmed","services:\n  redis:\n    command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}\n\n  mta:\n    environment:\n      REDIS_URL: redis:\u002F\u002F:${REDIS_PASSWORD}@redis:6379\n","yaml",[43,567,568,577,584,595,599,606,613],{"__ignoreMap":45},[61,569,570,574],{"class":63,"line":64},[61,571,573],{"class":572},"sIAta","services",[61,575,576],{"class":67},":\n",[61,578,579,582],{"class":63,"line":79},[61,580,581],{"class":572},"  redis",[61,583,576],{"class":67},[61,585,586,589,592],{"class":63,"line":90},[61,587,588],{"class":572},"    command",[61,590,591],{"class":67},": ",[61,593,594],{"class":75},"redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}\n",[61,596,597],{"class":63,"line":208},[61,598,224],{"emptyLinePlaceholder":223},[61,600,601,604],{"class":63,"line":214},[61,602,603],{"class":572},"  mta",[61,605,576],{"class":67},[61,607,608,611],{"class":63,"line":220},[61,609,610],{"class":572},"    environment",[61,612,576],{"class":67},[61,614,615,618,620],{"class":63,"line":227},[61,616,617],{"class":572},"      REDIS_URL",[61,619,591],{"class":67},[61,621,622],{"class":75},"redis:\u002F\u002F:${REDIS_PASSWORD}@redis:6379\n",[10,624,625,626,629,630,560],{},"Add ",[43,627,628],{},"REDIS_PASSWORD"," to your ",[43,631,51],{},[36,633,635],{"className":55,"code":634,"language":57,"meta":45,"style":45},"# Generate a Redis password\necho \"REDIS_PASSWORD=$(openssl rand -base64 32)\" >> .env\n",[43,636,637,642],{"__ignoreMap":45},[61,638,639],{"class":63,"line":64},[61,640,641],{"class":372},"# Generate a Redis password\n",[61,643,644,647,650,653,656,659,662,665,668],{"class":63,"line":79},[61,645,646],{"class":130},"echo",[61,648,649],{"class":75}," \"REDIS_PASSWORD=$(",[61,651,652],{"class":111},"openssl",[61,654,655],{"class":75}," rand ",[61,657,658],{"class":130},"-base64",[61,660,661],{"class":130}," 32",[61,663,664],{"class":75},")\"",[61,666,667],{"class":71}," >>",[61,669,670],{"class":75}," .env\n",[10,672,673],{},"Restart the stack:",[36,675,677],{"className":55,"code":676,"language":57,"meta":45,"style":45},"docker compose up -d\n",[43,678,679],{"__ignoreMap":45},[61,680,681,683,685,688],{"class":63,"line":64},[61,682,502],{"class":111},[61,684,505],{"class":75},[61,686,687],{"class":75}," up",[61,689,690],{"class":130}," -d\n",[20,692,694],{"id":693},"backups","Backups",[28,696,698],{"id":697},"convex-data-critical","Convex Data (Critical)",[10,700,701,702,705],{},"The ",[43,703,704],{},"convex-data"," volume contains all application data — contacts, campaigns, templates, file uploads, and settings.",[10,707,708,711],{},[465,709,710],{},"Volume snapshot"," (if your hosting provider supports it):",[36,713,715],{"className":55,"code":714,"language":57,"meta":45,"style":45},"# Hetzner, AWS, etc. — create a volume\u002Fdisk snapshot via their API or console\n",[43,716,717],{"__ignoreMap":45},[61,718,719],{"class":63,"line":64},[61,720,714],{"class":372},[10,722,723,726],{},[465,724,725],{},"Manual backup"," (requires brief downtime):",[36,728,730],{"className":55,"code":729,"language":57,"meta":45,"style":45},"# Stop Convex to ensure consistency\ndocker compose stop convex\n\n# Back up the volume\ndocker run --rm -v owlat_convex-data:\u002Fdata -v $(pwd)\u002Fbackups:\u002Fbackup \\\n  alpine tar czf \u002Fbackup\u002Fconvex-data-$(date +%Y%m%d).tar.gz -C \u002Fdata .\n\n# Start Convex again\ndocker compose start convex\n",[43,731,732,737,749,753,758,791,828,832,837],{"__ignoreMap":45},[61,733,734],{"class":63,"line":64},[61,735,736],{"class":372},"# Stop Convex to ensure consistency\n",[61,738,739,741,743,746],{"class":63,"line":79},[61,740,502],{"class":111},[61,742,505],{"class":75},[61,744,745],{"class":75}," stop",[61,747,748],{"class":75}," convex\n",[61,750,751],{"class":63,"line":90},[61,752,224],{"emptyLinePlaceholder":223},[61,754,755],{"class":63,"line":208},[61,756,757],{"class":372},"# Back up the volume\n",[61,759,760,762,765,768,771,774,776,779,782,784,787],{"class":63,"line":214},[61,761,502],{"class":111},[61,763,764],{"class":75}," run",[61,766,767],{"class":130}," --rm",[61,769,770],{"class":130}," -v",[61,772,773],{"class":75}," owlat_convex-data:\u002Fdata",[61,775,770],{"class":130},[61,777,778],{"class":67}," $(",[61,780,781],{"class":130},"pwd",[61,783,485],{"class":67},[61,785,786],{"class":75},"\u002Fbackups:\u002Fbackup",[61,788,790],{"class":789},"s74oq"," \\\n",[61,792,793,796,799,802,805,808,811,814,816,819,822,825],{"class":63,"line":220},[61,794,795],{"class":75},"  alpine",[61,797,798],{"class":75}," tar",[61,800,801],{"class":75}," czf",[61,803,804],{"class":75}," \u002Fbackup\u002Fconvex-data-",[61,806,807],{"class":67},"$(",[61,809,810],{"class":111},"date",[61,812,813],{"class":75}," +%Y%m%d",[61,815,485],{"class":67},[61,817,818],{"class":75},".tar.gz",[61,820,821],{"class":130}," -C",[61,823,824],{"class":75}," \u002Fdata",[61,826,827],{"class":75}," .\n",[61,829,830],{"class":63,"line":227},[61,831,224],{"emptyLinePlaceholder":223},[61,833,834],{"class":63,"line":232},[61,835,836],{"class":372},"# Start Convex again\n",[61,838,839,841,843,846],{"class":63,"line":238},[61,840,502],{"class":111},[61,842,505],{"class":75},[61,844,845],{"class":75}," start",[61,847,748],{"class":75},[28,849,851],{"id":850},"redis-data-medium-priority","Redis Data (Medium Priority)",[10,853,854,855,858],{},"Redis uses AOF (Append Only File) persistence by default. The data is automatically persisted to the ",[43,856,857],{},"redis-data"," volume.",[36,860,862],{"className":55,"code":861,"language":57,"meta":45,"style":45},"# Backup without stopping Redis\ndocker run --rm -v owlat_redis-data:\u002Fdata -v $(pwd)\u002Fbackups:\u002Fbackup \\\n  alpine cp \u002Fdata\u002Fappendonly.aof \u002Fbackup\u002Fredis-aof-$(date +%Y%m%d).aof\n",[43,863,864,869,894],{"__ignoreMap":45},[61,865,866],{"class":63,"line":64},[61,867,868],{"class":372},"# Backup without stopping Redis\n",[61,870,871,873,875,877,879,882,884,886,888,890,892],{"class":63,"line":79},[61,872,502],{"class":111},[61,874,764],{"class":75},[61,876,767],{"class":130},[61,878,770],{"class":130},[61,880,881],{"class":75}," owlat_redis-data:\u002Fdata",[61,883,770],{"class":130},[61,885,778],{"class":67},[61,887,781],{"class":130},[61,889,485],{"class":67},[61,891,786],{"class":75},[61,893,790],{"class":789},[61,895,896,898,901,904,907,909,911,913,915],{"class":63,"line":90},[61,897,795],{"class":75},[61,899,900],{"class":75}," cp",[61,902,903],{"class":75}," \u002Fdata\u002Fappendonly.aof",[61,905,906],{"class":75}," \u002Fbackup\u002Fredis-aof-",[61,908,807],{"class":67},[61,910,810],{"class":111},[61,912,813],{"class":75},[61,914,485],{"class":67},[61,916,917],{"class":75},".aof\n",[28,919,921],{"id":920},"clamav-data-low-priority","ClamAV Data (Low Priority)",[10,923,924],{},"Virus definition signatures are downloaded automatically by freshclamd. No backup needed — they re-download on startup.",[20,926,928],{"id":927},"monitoring","Monitoring",[28,930,932],{"id":931},"health-checks","Health Checks",[10,934,935],{},"Docker healthchecks are already configured for the critical services (Convex, Redis, ClamAV). Check their status:",[36,937,939],{"className":55,"code":938,"language":57,"meta":45,"style":45},"docker compose ps\n",[43,940,941],{"__ignoreMap":45},[61,942,943,945,947],{"class":63,"line":64},[61,944,502],{"class":111},[61,946,505],{"class":75},[61,948,949],{"class":75}," ps\n",[10,951,952],{},"For external monitoring, create a health check script:",[36,954,956],{"className":55,"code":955,"language":57,"meta":45,"style":45},"#!\u002Fbin\u002Fbash\n# health-check.sh\n\nCONVEX_OK=$(curl -sf http:\u002F\u002Flocalhost:3210\u002Fversion && echo \"ok\" || echo \"fail\")\nWEB_OK=$(curl -sf http:\u002F\u002Flocalhost:3000 && echo \"ok\" || echo \"fail\")\nMTA_OK=$(curl -sf http:\u002F\u002Flocalhost:3100\u002Fhealth && echo \"ok\" || echo \"fail\")\n\necho \"Convex: $CONVEX_OK | Web: $WEB_OK | MTA: $MTA_OK\"\n\nif [[ \"$CONVEX_OK\" != \"ok\" || \"$WEB_OK\" != \"ok\" || \"$MTA_OK\" != \"ok\" ]]; then\n  exit 1  # Unhealthy — trigger your alerting system\nfi\n",[43,957,958,963,968,972,1010,1040,1070,1074,1099,1103,1154,1165],{"__ignoreMap":45},[61,959,960],{"class":63,"line":64},[61,961,962],{"class":372},"#!\u002Fbin\u002Fbash\n",[61,964,965],{"class":63,"line":79},[61,966,967],{"class":372},"# health-check.sh\n",[61,969,970],{"class":63,"line":90},[61,971,224],{"emptyLinePlaceholder":223},[61,973,974,977,979,981,984,987,990,993,995,998,1001,1004,1007],{"class":63,"line":208},[61,975,976],{"class":67},"CONVEX_OK",[61,978,72],{"class":71},[61,980,807],{"class":67},[61,982,983],{"class":111},"curl",[61,985,986],{"class":130}," -sf",[61,988,989],{"class":75}," http:\u002F\u002Flocalhost:3210\u002Fversion",[61,991,992],{"class":67}," && ",[61,994,646],{"class":130},[61,996,997],{"class":75}," \"ok\"",[61,999,1000],{"class":71}," ||",[61,1002,1003],{"class":130}," echo",[61,1005,1006],{"class":75}," \"fail\"",[61,1008,1009],{"class":67},")\n",[61,1011,1012,1015,1017,1019,1021,1023,1026,1028,1030,1032,1034,1036,1038],{"class":63,"line":214},[61,1013,1014],{"class":67},"WEB_OK",[61,1016,72],{"class":71},[61,1018,807],{"class":67},[61,1020,983],{"class":111},[61,1022,986],{"class":130},[61,1024,1025],{"class":75}," http:\u002F\u002Flocalhost:3000",[61,1027,992],{"class":67},[61,1029,646],{"class":130},[61,1031,997],{"class":75},[61,1033,1000],{"class":71},[61,1035,1003],{"class":130},[61,1037,1006],{"class":75},[61,1039,1009],{"class":67},[61,1041,1042,1045,1047,1049,1051,1053,1056,1058,1060,1062,1064,1066,1068],{"class":63,"line":220},[61,1043,1044],{"class":67},"MTA_OK",[61,1046,72],{"class":71},[61,1048,807],{"class":67},[61,1050,983],{"class":111},[61,1052,986],{"class":130},[61,1054,1055],{"class":75}," http:\u002F\u002Flocalhost:3100\u002Fhealth",[61,1057,992],{"class":67},[61,1059,646],{"class":130},[61,1061,997],{"class":75},[61,1063,1000],{"class":71},[61,1065,1003],{"class":130},[61,1067,1006],{"class":75},[61,1069,1009],{"class":67},[61,1071,1072],{"class":63,"line":227},[61,1073,224],{"emptyLinePlaceholder":223},[61,1075,1076,1078,1081,1084,1087,1090,1093,1096],{"class":63,"line":232},[61,1077,646],{"class":130},[61,1079,1080],{"class":75}," \"Convex: ",[61,1082,1083],{"class":67},"$CONVEX_OK",[61,1085,1086],{"class":75}," | Web: ",[61,1088,1089],{"class":67},"$WEB_OK",[61,1091,1092],{"class":75}," | MTA: ",[61,1094,1095],{"class":67},"$MTA_OK",[61,1097,1098],{"class":75},"\"\n",[61,1100,1101],{"class":63,"line":238},[61,1102,224],{"emptyLinePlaceholder":223},[61,1104,1105,1108,1111,1114,1116,1118,1121,1123,1125,1128,1130,1132,1134,1136,1138,1140,1142,1144,1146,1148,1151],{"class":63,"line":243},[61,1106,1107],{"class":71},"if",[61,1109,1110],{"class":67}," [[ ",[61,1112,1113],{"class":75},"\"",[61,1115,1083],{"class":67},[61,1117,1113],{"class":75},[61,1119,1120],{"class":71}," !=",[61,1122,997],{"class":75},[61,1124,1000],{"class":71},[61,1126,1127],{"class":75}," \"",[61,1129,1089],{"class":67},[61,1131,1113],{"class":75},[61,1133,1120],{"class":71},[61,1135,997],{"class":75},[61,1137,1000],{"class":71},[61,1139,1127],{"class":75},[61,1141,1095],{"class":67},[61,1143,1113],{"class":75},[61,1145,1120],{"class":71},[61,1147,997],{"class":75},[61,1149,1150],{"class":67}," ]]; ",[61,1152,1153],{"class":71},"then\n",[61,1155,1156,1159,1162],{"class":63,"line":248},[61,1157,1158],{"class":130},"  exit",[61,1160,1161],{"class":130}," 1",[61,1163,1164],{"class":372},"  # Unhealthy — trigger your alerting system\n",[61,1166,1167],{"class":63,"line":254},[61,1168,1169],{"class":71},"fi\n",[28,1171,1173],{"id":1172},"mta-metrics","MTA Metrics",[10,1175,1176],{},"The MTA exposes Prometheus-compatible metrics:",[36,1178,1180],{"className":55,"code":1179,"language":57,"meta":45,"style":45},"curl http:\u002F\u002Flocalhost:3100\u002Fmetrics\n",[43,1181,1182],{"__ignoreMap":45},[61,1183,1184,1186],{"class":63,"line":64},[61,1185,983],{"class":111},[61,1187,1188],{"class":75}," http:\u002F\u002Flocalhost:3100\u002Fmetrics\n",[10,1190,1191],{},"Connect this to Prometheus + Grafana for dashboards covering send rates, bounce rates, and queue depth.",[20,1193,1195],{"id":1194},"resource-requirements","Resource Requirements",[1197,1198,1199,1221],"table",{},[1200,1201,1202],"thead",{},[1203,1204,1205,1209,1212,1215,1218],"tr",{},[1206,1207,1208],"th",{},"Tier",[1206,1210,1211],{},"vCPU",[1206,1213,1214],{},"RAM",[1206,1216,1217],{},"Disk",[1206,1219,1220],{},"Suitable For",[1222,1223,1224,1242,1259],"tbody",{},[1203,1225,1226,1230,1233,1236,1239],{},[1227,1228,1229],"td",{},"Starter",[1227,1231,1232],{},"2",[1227,1234,1235],{},"4 GB",[1227,1237,1238],{},"40 GB",[1227,1240,1241],{},"Up to 10,000 contacts, light sending",[1203,1243,1244,1247,1250,1253,1256],{},[1227,1245,1246],{},"Growth",[1227,1248,1249],{},"4",[1227,1251,1252],{},"8 GB",[1227,1254,1255],{},"80 GB",[1227,1257,1258],{},"Up to 100,000 contacts, regular campaigns",[1203,1260,1261,1264,1267,1270,1273],{},[1227,1262,1263],{},"Enterprise",[1227,1265,1266],{},"8",[1227,1268,1269],{},"16 GB",[1227,1271,1272],{},"160 GB",[1227,1274,1275],{},"100,000+ contacts, high-volume sending",[10,1277,1278,1279,1282,1283,1286],{},"ClamAV uses approximately 1 GB of RAM for virus definitions. If memory is tight, you can disable ClamAV by removing it from the Docker Compose file and skipping the ",[43,1280,1281],{},"CLAMAV_HOST","\u002F",[43,1284,1285],{},"CLAMAV_PORT"," configuration.",[1288,1289,1290],"style",{},"html pre.shiki code .sYgZi, html code.shiki .sYgZi{--shiki-default:#24292E;--shiki-dark:#ADBAC7}html pre.shiki code .s7YZ4, html code.shiki .s7YZ4{--shiki-default:#D73A49;--shiki-dark:#F47067}html pre.shiki code .s-HuK, html code.shiki .s-HuK{--shiki-default:#032F62;--shiki-dark:#96D0FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sOLd2, html code.shiki .sOLd2{--shiki-default:#6F42C1;--shiki-dark:#F69D50}html pre.shiki code .sviXB, html code.shiki .sviXB{--shiki-default:#005CC5;--shiki-dark:#6CB6FF}html pre.shiki code .sDN9O, html code.shiki .sDN9O{--shiki-default:#6A737D;--shiki-dark:#768390}html pre.shiki code .s74oq, html code.shiki .s74oq{--shiki-default:#005CC5;--shiki-dark:#F47067}html pre.shiki code .sIAta, html code.shiki .sIAta{--shiki-default:#22863A;--shiki-dark:#8DDB8C}",{"title":45,"searchDepth":79,"depth":79,"links":1292},[1293,1297,1298,1299,1300,1305,1309],{"id":22,"depth":79,"text":23,"children":1294},[1295,1296],{"id":30,"depth":90,"text":31},{"id":182,"depth":90,"text":183},{"id":358,"depth":79,"text":359},{"id":471,"depth":79,"text":472},{"id":552,"depth":79,"text":553},{"id":693,"depth":79,"text":694,"children":1301},[1302,1303,1304],{"id":697,"depth":90,"text":698},{"id":850,"depth":90,"text":851},{"id":920,"depth":90,"text":921},{"id":927,"depth":79,"text":928,"children":1306},[1307,1308],{"id":931,"depth":90,"text":932},{"id":1172,"depth":90,"text":1173},{"id":1194,"depth":79,"text":1195},"Secure your self-hosted Owlat instance with TLS, firewall rules, backups, and monitoring.","md",{},"\u002Fdeveloper\u002Fself-hosting-production",{"title":5,"description":1310},"3.developer\u002F33.self-hosting-production","kAKu6ya3md1qXQn4_hwWHFPGhjmSvhzoVmJJ28z6Wjg",[1318,1322],{"title":1319,"path":1320,"stem":1321,"children":-1},"DNS & Email Setup","\u002Fdeveloper\u002Fself-hosting-dns-email","3.developer\u002F32.self-hosting-dns-email",{"title":1323,"path":1324,"stem":1325,"children":-1},"Maintenance & Updates","\u002Fdeveloper\u002Fself-hosting-maintenance","3.developer\u002F34.self-hosting-maintenance",1777110578737]