[{"data":1,"prerenderedAt":505},["ShallowReactive",2],{"doc-\u002Fdocs\u002Fself-hosting\u002Fproduction":3,"github-stars":502},{"id":4,"title":5,"body":6,"description":494,"extension":495,"meta":496,"navigation":193,"order":160,"path":497,"section":498,"seo":499,"stem":500,"__hash__":501},"docs\u002Fdocs\u002Fself-hosting\u002Fproduction.md","Going to production",{"type":7,"value":8,"toc":486},"minimark",[9,13,22,27,34,99,126,130,133,136,164,167,247,250,274,288,292,295,348,351,355,358,361,372,376,385,389,476,482],[10,11,5],"h1",{"id":12},"going-to-production",[14,15,16,17,21],"p",{},"The defaults are tuned for trying Clerq on ",[18,19,20],"code",{},"localhost",". Before anyone else\ncan reach your instance, there are a few things to get right: real secrets,\nHTTPS, and the correct public URL. This page walks through all of it.",[23,24,26],"h2",{"id":25},"_1-set-real-secrets","1. Set real secrets",[14,28,29,30,33],{},"Never expose an instance with the default password and dev secret. In your\n",[18,31,32],{},".env"," next to the compose file:",[35,36,41],"pre",{"className":37,"code":38,"language":39,"meta":40,"style":40},"language-bash shiki shiki-themes github-dark","POSTGRES_PASSWORD=a-strong-random-password\nBETTER_AUTH_SECRET=$(openssl rand -base64 32)\nBETTER_AUTH_URL=https:\u002F\u002Fclerq.example.com\n","bash","",[18,42,43,60,88],{"__ignoreMap":40},[44,45,48,52,56],"span",{"class":46,"line":47},"line",1,[44,49,51],{"class":50},"s95oV","POSTGRES_PASSWORD",[44,53,55],{"class":54},"snl16","=",[44,57,59],{"class":58},"sU2Wk","a-strong-random-password\n",[44,61,63,66,68,71,75,78,82,85],{"class":46,"line":62},2,[44,64,65],{"class":50},"BETTER_AUTH_SECRET",[44,67,55],{"class":54},[44,69,70],{"class":50},"$(",[44,72,74],{"class":73},"svObZ","openssl",[44,76,77],{"class":58}," rand",[44,79,81],{"class":80},"sDLfK"," -base64",[44,83,84],{"class":80}," 32",[44,86,87],{"class":50},")\n",[44,89,91,94,96],{"class":46,"line":90},3,[44,92,93],{"class":50},"BETTER_AUTH_URL",[44,95,55],{"class":54},[44,97,98],{"class":58},"https:\u002F\u002Fclerq.example.com\n",[100,101,102,112,117],"ul",{},[103,104,105,107,108,111],"li",{},[18,106,51],{}," - the database password. The database port is bound to\n",[18,109,110],{},"127.0.0.1"," in the compose file, so it is not directly reachable from the\nnetwork, but a strong password is still the right baseline.",[103,113,114,116],{},[18,115,65],{}," - signs sessions and invoice-PDF links. Keep it secret\nand stable; rotating it logs everyone out.",[103,118,119,121,122,125],{},[18,120,93],{}," - the exact external origin users will type, including\n",[18,123,124],{},"https:\u002F\u002F",". Auth callbacks and redirects depend on this being correct.",[23,127,129],{"id":128},"_2-put-it-behind-a-reverse-proxy-with-https","2. Put it behind a reverse proxy with HTTPS",[14,131,132],{},"The app speaks plain HTTP on port 3000. In production you terminate TLS at a\nreverse proxy in front of it (Caddy, nginx, Traefik, or a cloud load\nbalancer) and proxy to the app.",[14,134,135],{},"The simplest option is Caddy, which obtains and renews certificates\nautomatically:",[35,137,141],{"className":138,"code":139,"language":140,"meta":40,"style":40},"language-caddy shiki shiki-themes github-dark","# Caddyfile\nclerq.example.com {\n    reverse_proxy 127.0.0.1:3000\n}\n","caddy",[18,142,143,148,153,158],{"__ignoreMap":40},[44,144,145],{"class":46,"line":47},[44,146,147],{},"# Caddyfile\n",[44,149,150],{"class":46,"line":62},[44,151,152],{},"clerq.example.com {\n",[44,154,155],{"class":46,"line":90},[44,156,157],{},"    reverse_proxy 127.0.0.1:3000\n",[44,159,161],{"class":46,"line":160},4,[44,162,163],{},"}\n",[14,165,166],{},"An equivalent nginx server block:",[35,168,172],{"className":169,"code":170,"language":171,"meta":40,"style":40},"language-nginx shiki shiki-themes github-dark","server {\n    listen 443 ssl;\n    server_name clerq.example.com;\n\n    # ssl_certificate \u002F ssl_certificate_key managed by certbot or similar\n\n    location \u002F {\n        proxy_pass http:\u002F\u002F127.0.0.1:3000;\n        proxy_set_header Host $host;\n        proxy_set_header X-Forwarded-Proto $scheme;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    }\n}\n","nginx",[18,173,174,179,184,189,195,201,206,212,218,224,230,236,242],{"__ignoreMap":40},[44,175,176],{"class":46,"line":47},[44,177,178],{},"server {\n",[44,180,181],{"class":46,"line":62},[44,182,183],{},"    listen 443 ssl;\n",[44,185,186],{"class":46,"line":90},[44,187,188],{},"    server_name clerq.example.com;\n",[44,190,191],{"class":46,"line":160},[44,192,194],{"emptyLinePlaceholder":193},true,"\n",[44,196,198],{"class":46,"line":197},5,[44,199,200],{},"    # ssl_certificate \u002F ssl_certificate_key managed by certbot or similar\n",[44,202,204],{"class":46,"line":203},6,[44,205,194],{"emptyLinePlaceholder":193},[44,207,209],{"class":46,"line":208},7,[44,210,211],{},"    location \u002F {\n",[44,213,215],{"class":46,"line":214},8,[44,216,217],{},"        proxy_pass http:\u002F\u002F127.0.0.1:3000;\n",[44,219,221],{"class":46,"line":220},9,[44,222,223],{},"        proxy_set_header Host $host;\n",[44,225,227],{"class":46,"line":226},10,[44,228,229],{},"        proxy_set_header X-Forwarded-Proto $scheme;\n",[44,231,233],{"class":46,"line":232},11,[44,234,235],{},"        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n",[44,237,239],{"class":46,"line":238},12,[44,240,241],{},"    }\n",[44,243,245],{"class":46,"line":244},13,[44,246,163],{},[14,248,249],{},"Whichever proxy you use, make sure:",[100,251,252,259,266],{},[103,253,254,255,258],{},"it forwards the original ",[18,256,257],{},"Host"," header,",[103,260,261,262,265],{},"it sets ",[18,263,264],{},"X-Forwarded-Proto: https",", and",[103,267,268,270,271,273],{},[18,269,93],{}," matches the public ",[18,272,124],{}," address exactly.",[14,275,276,277,280,281,283,284,287],{},"If you change ",[18,278,279],{},"APP_PORT"," in your ",[18,282,32],{},", point the proxy at that host port\ninstead of ",[18,285,286],{},"3000",".",[23,289,291],{"id":290},"_3-optional-enable-google-sign-in","3. Optional: enable Google sign-in",[14,293,294],{},"Email and password sign-in always works, so this step is optional. To add a\n\"Continue with Google\" button:",[296,297,298,301,311,342],"ol",{},[103,299,300],{},"In the Google Cloud console, create an OAuth 2.0 Client ID (type: Web\napplication).",[103,302,303,304,307,308,310],{},"Add the authorized redirect URI:\n",[18,305,306],{},"https:\u002F\u002Fclerq.example.com\u002Fapi\u002Fauth\u002Fcallback\u002Fgoogle"," (use your real\n",[18,309,93],{},").",[103,312,313,314,316,317],{},"Put the credentials in your ",[18,315,32],{},":",[35,318,320],{"className":37,"code":319,"language":39,"meta":40,"style":40},"GOOGLE_CLIENT_ID=your-client-id\nGOOGLE_CLIENT_SECRET=your-client-secret\n",[18,321,322,332],{"__ignoreMap":40},[44,323,324,327,329],{"class":46,"line":47},[44,325,326],{"class":50},"GOOGLE_CLIENT_ID",[44,328,55],{"class":54},[44,330,331],{"class":58},"your-client-id\n",[44,333,334,337,339],{"class":46,"line":62},[44,335,336],{"class":50},"GOOGLE_CLIENT_SECRET",[44,338,55],{"class":54},[44,340,341],{"class":58},"your-client-secret\n",[103,343,344,345,287],{},"Restart: ",[18,346,347],{},"docker compose up -d",[14,349,350],{},"The button appears automatically once both values are present, and disappears\nif you remove them.",[23,352,354],{"id":353},"_4-think-about-open-sign-ups","4. Think about open sign-ups",[14,356,357],{},"Clerq has no built-in switch to disable registration today. Any visitor who\ncan reach the sign-in page can create an account (they will not see your\nbusiness data - everything is scoped per business and new accounts start\nempty), but on a public instance you may still not want open registration.",[14,359,360],{},"Options:",[100,362,363,366,369],{},[103,364,365],{},"Keep the instance on a private network or VPN.",[103,367,368],{},"Put an authentication layer (your proxy's basic auth, an identity-aware\nproxy, or an allowlist) in front of it.",[103,370,371],{},"Run it public and accept that anyone can register their own empty business.",[23,373,375],{"id":374},"_5-back-it-up","5. Back it up",[14,377,378,379,384],{},"Set up the nightly dump and off-host storage described in\n",[380,381,383],"a",{"href":382},"\u002Fdocs\u002Fself-hosting\u002Fbackups-and-upgrades","Backups & upgrades"," before you\nstart entering real data, not after.",[23,386,388],{"id":387},"pre-flight-checklist","Pre-flight checklist",[100,390,393,405,413,424,430,442,451,464,470],{"className":391},[392],"contains-task-list",[103,394,397,401,402,404],{"className":395},[396],"task-list-item",[398,399],"input",{"disabled":193,"type":400},"checkbox"," ",[18,403,51],{}," changed from the default",[103,406,408,401,410,412],{"className":407},[396],[398,409],{"disabled":193,"type":400},[18,411,65],{}," is a fresh 32+ character random value",[103,414,416,401,418,420,421,423],{"className":415},[396],[398,417],{"disabled":193,"type":400},[18,419,93],{}," is your real ",[18,422,124],{}," origin",[103,425,427,429],{"className":426},[396],[398,428],{"disabled":193,"type":400}," TLS terminates at a reverse proxy in front of the app",[103,431,433,435,436,438,439],{"className":432},[396],[398,434],{"disabled":193,"type":400}," The proxy forwards ",[18,437,257],{}," and ",[18,440,441],{},"X-Forwarded-Proto",[103,443,445,447,448],{"className":444},[396],[398,446],{"disabled":193,"type":400}," No low-entropy-secret warning in ",[18,449,450],{},"docker compose logs app",[103,452,454,456,457,460,461],{"className":453},[396],[398,455],{"disabled":193,"type":400}," A health check hits ",[18,458,459],{},"\u002Fapi\u002Ftrpc\u002Fhealth.ping"," and gets ",[18,462,463],{},"ok: true",[103,465,467,469],{"className":466},[396],[398,468],{"disabled":193,"type":400}," Nightly backups run and are stored off the host",[103,471,473,475],{"className":472},[396],[398,474],{"disabled":193,"type":400}," You have decided how to handle open sign-ups",[477,478,479],"blockquote",{},[14,480,481],{},"Once this list is green, you have a production instance: your data, your\ninfrastructure, fully under your control.",[483,484,485],"style",{},"html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}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);}",{"title":40,"searchDepth":90,"depth":90,"links":487},[488,489,490,491,492,493],{"id":25,"depth":62,"text":26},{"id":128,"depth":62,"text":129},{"id":290,"depth":62,"text":291},{"id":353,"depth":62,"text":354},{"id":374,"depth":62,"text":375},{"id":387,"depth":62,"text":388},"Take a local Clerq instance and expose it safely - HTTPS behind a reverse proxy, real secrets, optional Google SSO, and a pre-flight checklist.","md",{},"\u002Fdocs\u002Fself-hosting\u002Fproduction","Self-hosting",{"title":5,"description":494},"docs\u002Fself-hosting\u002Fproduction","kT8q8SAZnzhkyqVQXze-aTFU3INQYhrB0wLpIMRlR00",{"stars":503,"repo":504},0,"PunterDigital\u002Fclerq",1781535397977]