[{"data":1,"prerenderedAt":1201},["ShallowReactive",2],{"\u002Fblog\u002Fdeploying-laravel-with-k3s-and-argocd":3,"\u002Fblog\u002Fdeploying-laravel-with-k3s-and-argocd-surround":1199},{"id":4,"title":5,"author":6,"body":10,"date":1190,"description":1191,"extension":1192,"image":1193,"meta":1194,"minRead":329,"navigation":384,"path":1195,"seo":1196,"stem":1197,"__hash__":1198},"blog\u002Fblog\u002Fdeploying-laravel-with-k3s-and-argocd.md","A Minimal GitOps Architecture for Laravel on k3s",{"name":7,"avatar":8},"YL",{"src":9,"alt":7},"\u002Fprofile.png",{"type":11,"value":12,"toc":1176},"minimark",[13,17,20,51,54,59,70,73,79,82,86,92,95,99,198,201,205,208,214,217,223,226,230,233,338,341,416,427,430,462,465,471,474,478,481,609,612,615,777,780,784,787,793,796,799,879,882,888,891,936,940,943,949,952,977,980,986,989,993,996,1002,1005,1011,1014,1053,1056,1060,1063,1069,1072,1078,1081,1085,1088,1094,1097,1101,1104,1146,1149,1169,1172],[14,15,16],"p",{},"This is a minimal GitOps architecture for running several Laravel applications on a small VPS.",[14,18,19],{},"The target is practical:",[21,22,23,27,30,33,36,39,42,45,48],"ul",{},[24,25,26],"li",{},"one cheap vps",[24,28,29],{},"five Laravel apps",[24,31,32],{},"HTTPS",[24,34,35],{},"Redis",[24,37,38],{},"Postgres",[24,40,41],{},"file and database backups",[24,43,44],{},"uptime monitoring",[24,46,47],{},"rolling deployments",[24,49,50],{},"disaster recovery in less than 30 minutes",[14,52,53],{},"This is not high availability. It is a low-cost, reproducible setup where the recovery story is stronger than the server.",[55,56,58],"h2",{"id":57},"architecture","Architecture",[60,61,67],"pre",{"className":62,"code":64,"language":65,"meta":66},[63],"language-text","                              Internet\n                                  |\n                                  v\n                         +----------------+\n                         | Traefik + TLS  |\n                         +----------------+\n                                  |\n              +-------------------+-------------------+\n              |                   |                   |\n              v                   v                   v\n          Laravel A           Laravel B           Laravel C...\n              |                   |                   |\n      +-------+-------+   +-------+-------+   +-------+-------+\n      | web replicas  |   | web replicas  |   | web replicas  |\n      | queue worker  |   | queue worker  |   | queue worker  |\n      | scheduler job |   | scheduler job |   | scheduler job |\n      +-------+-------+   +-------+-------+   +-------+-------+\n              |                   |                   |\n              +----------+--------+--------+----------+\n                         |                 |\n                         v                 v\n                    +---------+       +----------+\n                    |  Redis  |       | Postgres |\n                    +---------+       +----------+\n                                           |\n                                           v\n                                    S3-compatible backups\n","text","",[68,69,64],"code",{"__ignoreMap":66},[14,71,72],{},"The cluster is small enough to reason about:",[60,74,77],{"className":75,"code":76,"language":65,"meta":66},[63],"system\n  Argo CD\n  cert-manager\n  sealed or external secrets\n  storage\n  backup controller\n\ndata\n  Postgres\n  Redis\n\nmonitoring\n  uptime checks\n\napps\n  app-a\n  app-b\n  app-c\n  app-d\n  app-e\n",[68,78,76],{"__ignoreMap":66},[14,80,81],{},"The rule is simple: shared infrastructure goes in shared namespaces, applications get their own namespaces, and everything important is recreated from Git.",[55,83,85],{"id":84},"gitops-flow","GitOps Flow",[60,87,90],{"className":88,"code":89,"language":65,"meta":66},[63],"Developer\n   |\n   | git push\n   v\nCI pipeline\n   |\n   | build image\n   | push image\n   v\nContainer registry\n   |\n   | update image tag or digest in Git\n   v\nGitOps repository\n   |\n   | Argo CD watches Git\n   v\nk3s cluster\n   |\n   | rolling update\n   | readiness probes gate traffic\n   v\nProduction\n",[68,91,89],{"__ignoreMap":66},[14,93,94],{},"The server should not be configured by hand after bootstrap. Hand changes disappear. Git changes survive.",[55,96,98],{"id":97},"tool-stack","Tool Stack",[100,101,102,115],"table",{},[103,104,105],"thead",{},[106,107,108,112],"tr",{},[109,110,111],"th",{},"Need",[109,113,114],{},"Tool",[116,117,118,127,135,143,151,159,167,174,182,190],"tbody",{},[106,119,120,124],{},[121,122,123],"td",{},"Lightweight Kubernetes",[121,125,126],{},"k3s",[106,128,129,132],{},[121,130,131],{},"GitOps deployment",[121,133,134],{},"Argo CD",[106,136,137,140],{},[121,138,139],{},"Ingress and routing",[121,141,142],{},"Traefik",[106,144,145,148],{},[121,146,147],{},"TLS certificates",[121,149,150],{},"cert-manager",[106,152,153,156],{},[121,154,155],{},"Secrets in Git",[121,157,158],{},"Sealed Secrets or External Secrets",[106,160,161,164],{},[121,162,163],{},"Database",[121,165,166],{},"CloudNativePG",[106,168,169,172],{},[121,170,171],{},"Cache and queues",[121,173,35],{},[106,175,176,179],{},[121,177,178],{},"Laravel files",[121,180,181],{},"Longhorn or local persistent volumes",[106,183,184,187],{},[121,185,186],{},"Cluster restore",[121,188,189],{},"Velero",[106,191,192,195],{},[121,193,194],{},"Public checks",[121,196,197],{},"Uptime Kuma",[14,199,200],{},"You can swap tools, but avoid removing the roles. A small platform still needs routing, secrets, storage, backups, and health checks.",[55,202,204],{"id":203},"the-laravel-unit","The Laravel Unit",[14,206,207],{},"Each Laravel app should look like the same small unit:",[60,209,212],{"className":210,"code":211,"language":65,"meta":66},[63],"app namespace\n  Ingress\n    -> Service\n      -> web Deployment, 2 replicas\n\n  queue Deployment, 1+ replicas\n  scheduler CronJob\n  app Secret\n  storage PVC\n  database declaration\n",[68,213,211],{"__ignoreMap":66},[14,215,216],{},"For five apps, repeat the same shape five times.",[60,218,221],{"className":219,"code":220,"language":65,"meta":66},[63],"apps\n  app-a  -> web + queue + scheduler + files + db\n  app-b  -> web + queue + scheduler + files + db\n  app-c  -> web + queue + scheduler + files + db\n  app-d  -> web + queue + scheduler + files + db\n  app-e  -> web + queue + scheduler + files + db\n",[68,222,220],{"__ignoreMap":66},[14,224,225],{},"The web process is only one part of Laravel. Treat queues and scheduled commands as first-class workloads.",[55,227,229],{"id":228},"web-deployment","Web Deployment",[14,231,232],{},"Run the web process with two replicas when you want zero-downtime deploys.",[60,234,238],{"className":235,"code":236,"language":237,"meta":66,"style":66},"language-yaml shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","apiVersion: apps\u002Fv1\nkind: Deployment\nspec:\n  replicas: 2\n  strategy:\n    type: RollingUpdate\n    rollingUpdate:\n      maxUnavailable: 0\n      maxSurge: 1\n","yaml",[68,239,240,257,268,277,289,297,308,316,327],{"__ignoreMap":66},[241,242,245,249,253],"span",{"class":243,"line":244},"line",1,[241,246,248],{"class":247},"swJcz","apiVersion",[241,250,252],{"class":251},"sMK4o",":",[241,254,256],{"class":255},"sfazB"," apps\u002Fv1\n",[241,258,260,263,265],{"class":243,"line":259},2,[241,261,262],{"class":247},"kind",[241,264,252],{"class":251},[241,266,267],{"class":255}," Deployment\n",[241,269,271,274],{"class":243,"line":270},3,[241,272,273],{"class":247},"spec",[241,275,276],{"class":251},":\n",[241,278,280,283,285],{"class":243,"line":279},4,[241,281,282],{"class":247},"  replicas",[241,284,252],{"class":251},[241,286,288],{"class":287},"sbssI"," 2\n",[241,290,292,295],{"class":243,"line":291},5,[241,293,294],{"class":247},"  strategy",[241,296,276],{"class":251},[241,298,300,303,305],{"class":243,"line":299},6,[241,301,302],{"class":247},"    type",[241,304,252],{"class":251},[241,306,307],{"class":255}," RollingUpdate\n",[241,309,311,314],{"class":243,"line":310},7,[241,312,313],{"class":247},"    rollingUpdate",[241,315,276],{"class":251},[241,317,319,322,324],{"class":243,"line":318},8,[241,320,321],{"class":247},"      maxUnavailable",[241,323,252],{"class":251},[241,325,326],{"class":287}," 0\n",[241,328,330,333,335],{"class":243,"line":329},9,[241,331,332],{"class":247},"      maxSurge",[241,334,252],{"class":251},[241,336,337],{"class":287}," 1\n",[14,339,340],{},"Traffic should only reach ready pods.",[60,342,344],{"className":235,"code":343,"language":237,"meta":66,"style":66},"livenessProbe:\n  httpGet:\n    path: \u002Fup\n    port: 8080\n\nreadinessProbe:\n  httpGet:\n    path: \u002Fready\n    port: 8080\n",[68,345,346,353,360,370,380,386,393,399,408],{"__ignoreMap":66},[241,347,348,351],{"class":243,"line":244},[241,349,350],{"class":247},"livenessProbe",[241,352,276],{"class":251},[241,354,355,358],{"class":243,"line":259},[241,356,357],{"class":247},"  httpGet",[241,359,276],{"class":251},[241,361,362,365,367],{"class":243,"line":270},[241,363,364],{"class":247},"    path",[241,366,252],{"class":251},[241,368,369],{"class":255}," \u002Fup\n",[241,371,372,375,377],{"class":243,"line":279},[241,373,374],{"class":247},"    port",[241,376,252],{"class":251},[241,378,379],{"class":287}," 8080\n",[241,381,382],{"class":243,"line":291},[241,383,385],{"emptyLinePlaceholder":384},true,"\n",[241,387,388,391],{"class":243,"line":299},[241,389,390],{"class":247},"readinessProbe",[241,392,276],{"class":251},[241,394,395,397],{"class":243,"line":310},[241,396,357],{"class":247},[241,398,276],{"class":251},[241,400,401,403,405],{"class":243,"line":318},[241,402,364],{"class":247},[241,404,252],{"class":251},[241,406,407],{"class":255}," \u002Fready\n",[241,409,410,412,414],{"class":243,"line":329},[241,411,374],{"class":247},[241,413,252],{"class":251},[241,415,379],{"class":287},[14,417,418,419,422,423,426],{},"Use ",[68,420,421],{},"\u002Fup"," for \"the process is alive\". Use ",[68,424,425],{},"\u002Fready"," for \"this pod can receive traffic\".",[14,428,429],{},"The app receives its environment from a Kubernetes secret:",[60,431,433],{"className":235,"code":432,"language":237,"meta":66,"style":66},"envFrom:\n  - secretRef:\n      name: app-env\n",[68,434,435,442,452],{"__ignoreMap":66},[241,436,437,440],{"class":243,"line":244},[241,438,439],{"class":247},"envFrom",[241,441,276],{"class":251},[241,443,444,447,450],{"class":243,"line":259},[241,445,446],{"class":251},"  -",[241,448,449],{"class":247}," secretRef",[241,451,276],{"class":251},[241,453,454,457,459],{"class":243,"line":270},[241,455,456],{"class":247},"      name",[241,458,252],{"class":251},[241,460,461],{"class":255}," app-env\n",[14,463,464],{},"The mutable Laravel directory should be a mounted volume:",[60,466,469],{"className":467,"code":468,"language":65,"meta":66},[63],"\u002Fvar\u002Fwww\u002Fhtml\u002Fstorage\u002Fapp\n",[68,470,468],{"__ignoreMap":66},[14,472,473],{},"Everything else should be inside the image.",[55,475,477],{"id":476},"queue-and-scheduler","Queue and Scheduler",[14,479,480],{},"Queues should not run inside the web pod.",[60,482,484],{"className":235,"code":483,"language":237,"meta":66,"style":66},"apiVersion: apps\u002Fv1\nkind: Deployment\nmetadata:\n  name: queue\nspec:\n  replicas: 1\n  template:\n    spec:\n      containers:\n        - name: queue\n          command: [\"php\", \"artisan\", \"horizon\"]\n",[68,485,486,494,502,509,519,525,533,540,547,554,567],{"__ignoreMap":66},[241,487,488,490,492],{"class":243,"line":244},[241,489,248],{"class":247},[241,491,252],{"class":251},[241,493,256],{"class":255},[241,495,496,498,500],{"class":243,"line":259},[241,497,262],{"class":247},[241,499,252],{"class":251},[241,501,267],{"class":255},[241,503,504,507],{"class":243,"line":270},[241,505,506],{"class":247},"metadata",[241,508,276],{"class":251},[241,510,511,514,516],{"class":243,"line":279},[241,512,513],{"class":247},"  name",[241,515,252],{"class":251},[241,517,518],{"class":255}," queue\n",[241,520,521,523],{"class":243,"line":291},[241,522,273],{"class":247},[241,524,276],{"class":251},[241,526,527,529,531],{"class":243,"line":299},[241,528,282],{"class":247},[241,530,252],{"class":251},[241,532,337],{"class":287},[241,534,535,538],{"class":243,"line":310},[241,536,537],{"class":247},"  template",[241,539,276],{"class":251},[241,541,542,545],{"class":243,"line":318},[241,543,544],{"class":247},"    spec",[241,546,276],{"class":251},[241,548,549,552],{"class":243,"line":329},[241,550,551],{"class":247},"      containers",[241,553,276],{"class":251},[241,555,557,560,563,565],{"class":243,"line":556},10,[241,558,559],{"class":251},"        -",[241,561,562],{"class":247}," name",[241,564,252],{"class":251},[241,566,518],{"class":255},[241,568,570,573,575,578,581,584,586,589,592,595,597,599,601,604,606],{"class":243,"line":569},11,[241,571,572],{"class":247},"          command",[241,574,252],{"class":251},[241,576,577],{"class":251}," [",[241,579,580],{"class":251},"\"",[241,582,583],{"class":255},"php",[241,585,580],{"class":251},[241,587,588],{"class":251},",",[241,590,591],{"class":251}," \"",[241,593,594],{"class":255},"artisan",[241,596,580],{"class":251},[241,598,588],{"class":251},[241,600,591],{"class":251},[241,602,603],{"class":255},"horizon",[241,605,580],{"class":251},[241,607,608],{"class":251},"]\n",[14,610,611],{},"If a queue is critical, Redis must be durable or the queue should use a durable backend.",[14,613,614],{},"The scheduler is a Kubernetes CronJob:",[60,616,618],{"className":235,"code":617,"language":237,"meta":66,"style":66},"apiVersion: batch\u002Fv1\nkind: CronJob\nmetadata:\n  name: scheduler\nspec:\n  schedule: \"* * * * *\"\n  concurrencyPolicy: Forbid\n  jobTemplate:\n    spec:\n      template:\n        spec:\n          restartPolicy: OnFailure\n          containers:\n            - name: scheduler\n              command: [\"php\", \"artisan\", \"schedule:run\"]\n",[68,619,620,629,638,644,653,659,674,684,691,697,704,711,722,730,742],{"__ignoreMap":66},[241,621,622,624,626],{"class":243,"line":244},[241,623,248],{"class":247},[241,625,252],{"class":251},[241,627,628],{"class":255}," batch\u002Fv1\n",[241,630,631,633,635],{"class":243,"line":259},[241,632,262],{"class":247},[241,634,252],{"class":251},[241,636,637],{"class":255}," CronJob\n",[241,639,640,642],{"class":243,"line":270},[241,641,506],{"class":247},[241,643,276],{"class":251},[241,645,646,648,650],{"class":243,"line":279},[241,647,513],{"class":247},[241,649,252],{"class":251},[241,651,652],{"class":255}," scheduler\n",[241,654,655,657],{"class":243,"line":291},[241,656,273],{"class":247},[241,658,276],{"class":251},[241,660,661,664,666,668,671],{"class":243,"line":299},[241,662,663],{"class":247},"  schedule",[241,665,252],{"class":251},[241,667,591],{"class":251},[241,669,670],{"class":255},"* * * * *",[241,672,673],{"class":251},"\"\n",[241,675,676,679,681],{"class":243,"line":310},[241,677,678],{"class":247},"  concurrencyPolicy",[241,680,252],{"class":251},[241,682,683],{"class":255}," Forbid\n",[241,685,686,689],{"class":243,"line":318},[241,687,688],{"class":247},"  jobTemplate",[241,690,276],{"class":251},[241,692,693,695],{"class":243,"line":329},[241,694,544],{"class":247},[241,696,276],{"class":251},[241,698,699,702],{"class":243,"line":556},[241,700,701],{"class":247},"      template",[241,703,276],{"class":251},[241,705,706,709],{"class":243,"line":569},[241,707,708],{"class":247},"        spec",[241,710,276],{"class":251},[241,712,714,717,719],{"class":243,"line":713},12,[241,715,716],{"class":247},"          restartPolicy",[241,718,252],{"class":251},[241,720,721],{"class":255}," OnFailure\n",[241,723,725,728],{"class":243,"line":724},13,[241,726,727],{"class":247},"          containers",[241,729,276],{"class":251},[241,731,733,736,738,740],{"class":243,"line":732},14,[241,734,735],{"class":251},"            -",[241,737,562],{"class":247},[241,739,252],{"class":251},[241,741,652],{"class":255},[241,743,745,748,750,752,754,756,758,760,762,764,766,768,770,773,775],{"class":243,"line":744},15,[241,746,747],{"class":247},"              command",[241,749,252],{"class":251},[241,751,577],{"class":251},[241,753,580],{"class":251},[241,755,583],{"class":255},[241,757,580],{"class":251},[241,759,588],{"class":251},[241,761,591],{"class":251},[241,763,594],{"class":255},[241,765,580],{"class":251},[241,767,588],{"class":251},[241,769,591],{"class":251},[241,771,772],{"class":255},"schedule:run",[241,774,580],{"class":251},[241,776,608],{"class":251},[14,778,779],{},"That replaces a permanent cron daemon inside the container.",[55,781,783],{"id":782},"database-and-redis","Database and Redis",[14,785,786],{},"Use one managed Postgres cluster inside Kubernetes and create one database per app.",[60,788,791],{"className":789,"code":790,"language":65,"meta":66},[63],"Postgres cluster\n  app_a database\n  app_b database\n  app_c database\n  app_d database\n  app_e database\n",[68,792,790],{"__ignoreMap":66},[14,794,795],{},"CloudNativePG gives you a Kubernetes-native Postgres controller, declarative databases, backups, and recovery workflows.",[14,797,798],{},"A database declaration can stay tiny:",[60,800,802],{"className":235,"code":801,"language":237,"meta":66,"style":66},"apiVersion: postgresql.cnpg.io\u002Fv1\nkind: Database\nmetadata:\n  name: app-a\nspec:\n  cluster:\n    name: postgres\n  name: app_a\n  owner: app\n",[68,803,804,813,822,828,837,843,850,860,869],{"__ignoreMap":66},[241,805,806,808,810],{"class":243,"line":244},[241,807,248],{"class":247},[241,809,252],{"class":251},[241,811,812],{"class":255}," postgresql.cnpg.io\u002Fv1\n",[241,814,815,817,819],{"class":243,"line":259},[241,816,262],{"class":247},[241,818,252],{"class":251},[241,820,821],{"class":255}," Database\n",[241,823,824,826],{"class":243,"line":270},[241,825,506],{"class":247},[241,827,276],{"class":251},[241,829,830,832,834],{"class":243,"line":279},[241,831,513],{"class":247},[241,833,252],{"class":251},[241,835,836],{"class":255}," app-a\n",[241,838,839,841],{"class":243,"line":291},[241,840,273],{"class":247},[241,842,276],{"class":251},[241,844,845,848],{"class":243,"line":299},[241,846,847],{"class":247},"  cluster",[241,849,276],{"class":251},[241,851,852,855,857],{"class":243,"line":310},[241,853,854],{"class":247},"    name",[241,856,252],{"class":251},[241,858,859],{"class":255}," postgres\n",[241,861,862,864,866],{"class":243,"line":318},[241,863,513],{"class":247},[241,865,252],{"class":251},[241,867,868],{"class":255}," app_a\n",[241,870,871,874,876],{"class":243,"line":329},[241,872,873],{"class":247},"  owner",[241,875,252],{"class":251},[241,877,878],{"class":255}," app\n",[14,880,881],{},"Redis can be shared by the apps:",[60,883,886],{"className":884,"code":885,"language":65,"meta":66},[63],"Redis\n  cache\n  sessions\n  queues\n  horizon\n",[68,887,885],{"__ignoreMap":66},[14,889,890],{},"For cheap setups, Redis is often the least protected service. Be explicit:",[100,892,893,903],{},[103,894,895],{},[106,896,897,900],{},[109,898,899],{},"Redis use",[109,901,902],{},"Persistence required?",[116,904,905,913,921,929],{},[106,906,907,910],{},[121,908,909],{},"cache",[121,911,912],{},"no",[106,914,915,918],{},[121,916,917],{},"disposable sessions",[121,919,920],{},"maybe",[106,922,923,926],{},[121,924,925],{},"queues",[121,927,928],{},"yes, unless losing jobs is acceptable",[106,930,931,934],{},[121,932,933],{},"Horizon metrics",[121,935,912],{},[55,937,939],{"id":938},"zero-downtime-deployments","Zero-Downtime Deployments",[14,941,942],{},"The deployment path should look like this:",[60,944,947],{"className":945,"code":946,"language":65,"meta":66},[63],"old pod ready\nold pod ready\n       |\n       | new image arrives\n       v\nold pod ready\nold pod ready\nnew pod starting\n       |\n       | readiness passes\n       v\nold pod ready\nnew pod ready\nnew pod ready\n       |\n       | old pods terminate\n       v\nnew pod ready\nnew pod ready\n",[68,948,946],{"__ignoreMap":66},[14,950,951],{},"The minimum requirements:",[21,953,954,957,962,965,968,971,974],{},[24,955,956],{},"two web replicas",[24,958,959],{},[68,960,961],{},"maxUnavailable: 0",[24,963,964],{},"a real readiness probe",[24,966,967],{},"enough memory for old and new pods during rollout",[24,969,970],{},"no destructive startup tasks",[24,972,973],{},"migrations run separately",[24,975,976],{},"database migrations are backward-compatible",[14,978,979],{},"The migration rule is the one people skip:",[60,981,984],{"className":982,"code":983,"language":65,"meta":66},[63],"deploy 1: add nullable column or new table\ndeploy 2: write code that uses it\ndeploy 3: remove old column only after old code is gone\n",[68,985,983],{"__ignoreMap":66},[14,987,988],{},"Do not make the new pod require a schema that breaks the old pod still receiving traffic.",[55,990,992],{"id":991},"backups","Backups",[14,994,995],{},"Backups need to cover four different things.",[60,997,1000],{"className":998,"code":999,"language":65,"meta":66},[63],"+-------------------+--------------------------+-------------------+\n| State             | Backup                   | Restore target    |\n+-------------------+--------------------------+-------------------+\n| Postgres data     | database backup + WAL    | Postgres cluster  |\n| Laravel files     | volume backup            | app PVC           |\n| Kubernetes state  | Velero                   | cluster resources |\n| Secrets           | sealed in Git + key copy | app secrets       |\n+-------------------+--------------------------+-------------------+\n",[68,1001,999],{"__ignoreMap":66},[14,1003,1004],{},"Object storage is the common target:",[60,1006,1009],{"className":1007,"code":1008,"language":65,"meta":66},[63],"k3s cluster\n  Postgres backups ----+\n  volume backups ------+----> S3-compatible bucket\n  Velero backups ------+\n",[68,1010,1008],{"__ignoreMap":66},[14,1012,1013],{},"The daily backup baseline:",[60,1015,1017],{"className":235,"code":1016,"language":237,"meta":66,"style":66},"schedule: \"10 2 * * *\"\nttl: 168h\ndestination: s3:\u002F\u002Fproduction-backups\n",[68,1018,1019,1033,1043],{"__ignoreMap":66},[241,1020,1021,1024,1026,1028,1031],{"class":243,"line":244},[241,1022,1023],{"class":247},"schedule",[241,1025,252],{"class":251},[241,1027,591],{"class":251},[241,1029,1030],{"class":255},"10 2 * * *",[241,1032,673],{"class":251},[241,1034,1035,1038,1040],{"class":243,"line":259},[241,1036,1037],{"class":247},"ttl",[241,1039,252],{"class":251},[241,1041,1042],{"class":255}," 168h\n",[241,1044,1045,1048,1050],{"class":243,"line":270},[241,1046,1047],{"class":247},"destination",[241,1049,252],{"class":251},[241,1051,1052],{"class":255}," s3:\u002F\u002Fproduction-backups\n",[14,1054,1055],{},"Seven days of retention is not magic. It is just a starting point. The important part is that database backups, file backups, and cluster metadata are all covered.",[55,1057,1059],{"id":1058},"disaster-recovery-in-30-minutes","Disaster Recovery in 30 Minutes",[14,1061,1062],{},"Fast recovery comes from reducing decisions during the incident.",[60,1064,1067],{"className":1065,"code":1066,"language":65,"meta":66},[63],"00:00  create a new VPS\n05:00  install k3s\n08:00  install Argo CD\n10:00  restore Git access and secret decryption key\n12:00  sync platform controllers\n15:00  restore Postgres and volumes from S3\n22:00  sync Laravel apps\n25:00  point DNS or floating IP to the new server\n30:00  uptime checks green\n",[68,1068,1066],{"__ignoreMap":66},[14,1070,1071],{},"The recovery diagram:",[60,1073,1076],{"className":1074,"code":1075,"language":65,"meta":66},[63],"Git repository                 S3 backups\n  platform manifests             postgres\n  app manifests                  files\n  sealed secrets                 cluster resources\n        |                              |\n        +--------------+---------------+\n                       |\n                       v\n                  new k3s server\n                       |\n                       v\n                  apps restored\n",[68,1077,1075],{"__ignoreMap":66},[14,1079,1080],{},"The cluster is disposable. Git and backups are not.",[55,1082,1084],{"id":1083},"minimal-monitoring","Minimal Monitoring",[14,1086,1087],{},"Start with checks you will actually react to.",[60,1089,1092],{"className":1090,"code":1091,"language":65,"meta":66},[63],"Uptime Kuma\n  GET https:\u002F\u002Fapp-a.example.com\u002Fup\n  GET https:\u002F\u002Fapp-b.example.com\u002Fup\n  GET https:\u002F\u002Fapp-c.example.com\u002Fup\n  GET https:\u002F\u002Fapp-d.example.com\u002Fup\n  GET https:\u002F\u002Fapp-e.example.com\u002Fup\n\nArgo CD\n  apps are Synced\n  apps are Healthy\n\nDatabase\n  cluster healthy\n  latest backup recent\n\nBackups\n  latest Velero backup completed\n  volume backup target reachable\n\nNode\n  disk not full\n  memory not exhausted\n",[68,1093,1091],{"__ignoreMap":66},[14,1095,1096],{},"Prometheus and Grafana are useful later. For the first version, public uptime checks plus backup freshness already catch the failures that matter most.",[55,1098,1100],{"id":1099},"final-checklist","Final Checklist",[14,1102,1103],{},"For each Laravel app:",[21,1105,1106,1109,1112,1115,1118,1121,1124,1127,1134,1137,1140,1143],{},[24,1107,1108],{},"one namespace",[24,1110,1111],{},"one web deployment with two replicas",[24,1113,1114],{},"one service",[24,1116,1117],{},"one ingress with TLS",[24,1119,1120],{},"one queue deployment if jobs are used",[24,1122,1123],{},"one scheduler CronJob",[24,1125,1126],{},"one environment secret",[24,1128,1129,1130,1133],{},"one PVC for ",[68,1131,1132],{},"storage\u002Fapp"," if files are local",[24,1135,1136],{},"one database",[24,1138,1139],{},"readiness and liveness probes",[24,1141,1142],{},"app included in uptime checks",[24,1144,1145],{},"app state included in backups",[14,1147,1148],{},"For the platform:",[21,1150,1151,1154,1157,1160,1163,1166],{},[24,1152,1153],{},"Argo CD can rebuild the cluster from Git",[24,1155,1156],{},"secrets can be decrypted after disaster recovery",[24,1158,1159],{},"Postgres backups restore successfully",[24,1161,1162],{},"file backups restore successfully",[24,1164,1165],{},"Velero can restore Kubernetes resources",[24,1167,1168],{},"DNS or IP failover is documented",[14,1170,1171],{},"That is the real architecture. Kubernetes is only the runtime. The system works because deployment, backups, and recovery are designed together.",[1173,1174,1175],"style",{},"html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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);}",{"title":66,"searchDepth":244,"depth":259,"links":1177},[1178,1179,1180,1181,1182,1183,1184,1185,1186,1187,1188,1189],{"id":57,"depth":259,"text":58},{"id":84,"depth":259,"text":85},{"id":97,"depth":259,"text":98},{"id":203,"depth":259,"text":204},{"id":228,"depth":259,"text":229},{"id":476,"depth":259,"text":477},{"id":782,"depth":259,"text":783},{"id":938,"depth":259,"text":939},{"id":991,"depth":259,"text":992},{"id":1058,"depth":259,"text":1059},{"id":1083,"depth":259,"text":1084},{"id":1099,"depth":259,"text":1100},"2026-05-11","A compact architecture for running several Laravel applications on one small VPS with k3s, Argo CD, Redis, Postgres, backups, uptime checks, and fast disaster recovery.","md","https:\u002F\u002Fimages.pexels.com\u002Fphotos\u002F11035380\u002Fpexels-photo-11035380.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1",{},"\u002Fblog\u002Fdeploying-laravel-with-k3s-and-argocd",{"title":5,"description":1191},"blog\u002Fdeploying-laravel-with-k3s-and-argocd","ssE0xtBASs870xVvqjkIXsJmWhvpaas77hTrjByJrZI",[1200,1200],null,1778661862632]