[{"data":1,"prerenderedAt":3015},["ShallowReactive",2],{"navigation":3,"blog":48,"\u002Fblog":61,"$f7Td39Vonkf02bgvz57NR3EcUtcG7bggq1tBSUZMiX14":3013},[4,23],{"title":5,"path":6,"stem":7,"children":8,"icon":22},"Getting Started","\u002Fdocs\u002Fgetting-started","1.docs\u002F1.getting-started\u002F1.index",[9,12,17],{"title":10,"path":6,"stem":7,"icon":11},"Introduction","i-lucide-house",{"title":13,"path":14,"stem":15,"icon":16},"Installation","\u002Fdocs\u002Fgetting-started\u002Finstallation","1.docs\u002F1.getting-started\u002F2.installation","i-lucide-download",{"title":18,"path":19,"stem":20,"icon":21},"Usage","\u002Fdocs\u002Fgetting-started\u002Fusage","1.docs\u002F1.getting-started\u002F3.usage","i-lucide-sliders",false,{"title":24,"path":25,"stem":26,"children":27,"page":22},"Essentials","\u002Fdocs\u002Fessentials","1.docs\u002F2.essentials",[28,33,38,43],{"title":29,"path":30,"stem":31,"icon":32},"Markdown Syntax","\u002Fdocs\u002Fessentials\u002Fmarkdown-syntax","1.docs\u002F2.essentials\u002F1.markdown-syntax","i-lucide-heading-1",{"title":34,"path":35,"stem":36,"icon":37},"Code Blocks","\u002Fdocs\u002Fessentials\u002Fcode-blocks","1.docs\u002F2.essentials\u002F2.code-blocks","i-lucide-code-xml",{"title":39,"path":40,"stem":41,"icon":42},"Prose Components","\u002Fdocs\u002Fessentials\u002Fprose-components","1.docs\u002F2.essentials\u002F3.prose-components","i-lucide-component",{"title":44,"path":45,"stem":46,"icon":47},"Images and Embeds","\u002Fdocs\u002Fessentials\u002Fimages-embeds","1.docs\u002F2.essentials\u002F4.images-embeds","i-lucide-image",{"id":49,"title":50,"body":51,"description":52,"extension":53,"meta":54,"navigation":55,"path":57,"seo":58,"stem":59,"__hash__":60},"blog\u002F3.blog.yml","Blog",null,"Discover the latest insights, tutorials, and updates from our team. Stay informed about web development trends, best practices, and innovative solutions.","yml",{},{"icon":56},"i-lucide-newspaper","\u002Fblog",{"title":50,"description":52},"3.blog","xU5p5VdJkx0QeTWiWoE__dOAG_p2L7hLKHVurHf_RyQ",[62,685,1071,1810,2856],{"id":63,"title":64,"authors":65,"badge":71,"body":73,"date":673,"description":674,"extension":675,"image":676,"meta":678,"navigation":679,"path":680,"seo":681,"stem":683,"__hash__":684},"posts\u002F3.blog\u002F1.bluetooth-pos.md","How I Architected a Modern POS System Using Laravel, Inertia.js, React, Midtrans, and Bluetooth Printing",[66],{"name":67,"to":68,"avatar":69},"Adi Sulaksono","https:\u002F\u002Fwww.github.com\u002Fadislksn\u002F",{"src":70},"\u002Fimg\u002Fprofile.png",{"label":72},"Laravel, React, POS",{"type":74,"value":75,"toc":659},"minimark",[76,80,104,115,120,123,151,154,185,189,192,250,304,359,362,366,372,386,444,451,455,466,469,486,530,534,537,567,570,574,577,603,607,610,613,642,645],[77,78,79],"p",{},"Building a point-of-sale system sounds straightforward until you actually ship one. Cashiers need speed. Owners need reports. Payments must be reliable. Receipts have to print instantly often over Bluetooth, on hardware that was never designed for the web.",[77,81,82,83,87,88,91,92,95,96,99,100,103],{},"This is how I architected a modern POS stack using ",[84,85,86],"strong",{},"Laravel"," on the backend, ",[84,89,90],{},"Inertia.js"," with ",[84,93,94],{},"React"," on the frontend, ",[84,97,98],{},"Midtrans"," for payments, and ",[84,101,102],{},"Bluetooth ESC\u002FPOS"," printing for thermal receipts.",[77,105,106],{},[107,108],"img",{"alt":109,"className":110,"height":112,"src":113,"width":114},"Modern point-of-sale terminal ready for checkout",[111],"rounded-lg",400,"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1742240216264-f0aac25ef4ba?w=1200&h=400&fit=crop",1200,[116,117,119],"h2",{"id":118},"why-this-stack","Why This Stack?",[77,121,122],{},"Most POS products are either legacy desktop apps or over engineered SaaS platforms. I wanted something in the middle:",[124,125,126,133,139,145],"ul",{},[127,128,129,132],"li",{},[84,130,131],{},"Fast UI"," for daily cashier workflows",[127,134,135,138],{},[84,136,137],{},"Single codebase"," for web admin and in-store checkout",[127,140,141,144],{},[84,142,143],{},"Indonesia ready payments"," via Midtrans",[127,146,147,150],{},[84,148,149],{},"Hardware support"," for Bluetooth thermal printers without native apps",[77,152,153],{},"Laravel + Inertia.js + React hit that balance. Laravel handles auth, inventory, orders, and webhooks. Inertia bridges server and client without a separate REST API layer for every screen. React delivers the interactive POS cart, product search, and checkout flow.",[155,156,157,167,176],"pictures",{},[158,159,160],"div",{},[77,161,162],{},[107,163],{"alt":164,"className":165,"height":112,"src":166,"width":112},"Developer workspace with laptop and code on screen",[111],"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1498050108023-c5249f4df085?w=400&h=400&fit=crop",[158,168,169],{},[77,170,171],{},[107,172],{"alt":173,"className":174,"height":112,"src":175,"width":112},"Payment terminal on a retail checkout counter",[111],"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1750262727446-759032bf283f?w=400&h=400&fit=crop",[158,177,178],{},[77,179,180],{},[107,181],{"alt":182,"className":183,"height":112,"src":184,"width":112},"Contactless card payment at point of sale",[111],"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1750263160599-53c7974bc79d?w=400&h=400&fit=crop",[116,186,188],{"id":187},"system-architecture","System Architecture",[77,190,191],{},"At a high level, the system splits into four layers:",[193,194,195,225],"tabs",{},[158,196,199],{"icon":197,"label":198},"i-lucide-layers","Layers",[124,200,201,207,213,219],{},[127,202,203,206],{},[84,204,205],{},"Presentation"," React pages rendered through Inertia.js (POS screen, product catalog, order history)",[127,208,209,212],{},[84,210,211],{},"Application"," Laravel controllers, form requests, policies, and service classes",[127,214,215,218],{},[84,216,217],{},"Domain"," Orders, line items, inventory movements, payment records, and receipt logs",[127,220,221,224],{},[84,222,223],{},"Integration"," Midtrans Snap\u002FCore API, Web Bluetooth ESC\u002FPOS, and optional queue workers",[158,226,229],{"icon":227,"label":228},"i-lucide-git-branch","Data Flow",[230,231,232,235,238,241,244,247],"ol",{},[127,233,234],{},"Cashier adds items to cart in React",[127,236,237],{},"Inertia posts checkout payload to Laravel",[127,239,240],{},"Laravel creates order + reserves stock inside a DB transaction",[127,242,243],{},"Midtrans payment token\u002Fcharge is generated when needed",[127,245,246],{},"Webhook confirms payment and marks order as paid",[127,248,249],{},"Frontend receives receipt payload and sends ESC\u002FPOS bytes to Bluetooth printer",[251,252,253,258,261,264,290,293],"picture-and-text",{},[254,255,257],"h3",{"id":256},"backend-laravel-as-the-source-of-truth","Backend: Laravel as the Source of Truth",[77,259,260],{},"Laravel owns every business rule. Products, categories, stock levels, discounts, tax, and user roles all live in the backend not in client-side state that can drift.",[77,262,263],{},"Key design choices:",[124,265,266,272,278,284],{},[127,267,268,271],{},[84,269,270],{},"Service classes"," for checkout, stock deduction, and payment reconciliation",[127,273,274,277],{},[84,275,276],{},"Database transactions"," so an order never commits without consistent inventory",[127,279,280,283],{},[84,281,282],{},"Policies"," to separate cashier, supervisor can, and owner permissions",[127,285,286,289],{},[84,287,288],{},"Webhook endpoint"," for Midtrans with idempotent payment status updates",[77,291,292],{},"The POS React app stays thin: it renders UI and sends validated payloads. Laravel decides what is allowed.",[294,295,297],"template",{"v-slot:image":296},"",[77,298,299],{},[107,300],{"alt":301,"className":302,"height":112,"src":303,"width":112},"Dashboard and analytics on a laptop screen",[111],"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1460925895917-afdab827c52f?w=400&h=400&fit=crop",[251,305,307,311,314,328,350],{":reverse":306},"true",[254,308,310],{"id":309},"frontend-inertiajs-react-for-the-pos-experience","Frontend: Inertia.js + React for the POS Experience",[77,312,313],{},"Inertia.js removed the need to build and maintain a separate REST API for every admin screen. Laravel returns page props; React renders them. For the POS floor, that meant:",[124,315,316,319,322,325],{},[127,317,318],{},"Instant product search and barcode friendly input",[127,320,321],{},"Cart state with quantity edits, notes, and discounts",[127,323,324],{},"Optimistic UI where safe, with server confirmation on checkout",[127,326,327],{},"Shared layouts for admin vs. cashier views",[77,329,330,331,335,336,335,339,335,342,345,346,349],{},"React components are organized by domain: ",[332,333,334],"code",{},"Cart",", ",[332,337,338],{},"ProductGrid",[332,340,341],{},"PaymentModal",[332,343,344],{},"ReceiptPreview",", and ",[332,347,348],{},"PrinterStatus",". Shared hooks handle keyboard shortcuts critical when cashiers process dozens of orders per hour.",[294,351,352],{"v-slot:image":296},[77,353,354],{},[107,355],{"alt":356,"className":357,"height":112,"src":358,"width":112},"Tablet-based point of sale on a wooden counter",[111],"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1762195954150-5db56b1d3a19?w=400&h=400&fit=crop",[77,360,361],{},"Decimal precision for money is handled server-side; the UI only displays formatted values from Laravel to avoid floating point bugs.",[116,363,365],{"id":364},"midtrans-payment-integration","Midtrans Payment Integration",[77,367,368,369,371],{},"For Indonesian merchants, ",[84,370,98],{}," is the practical choice. I integrated both:",[124,373,374,380],{},[127,375,376,379],{},[84,377,378],{},"Snap"," for hosted checkout flows when customers pay on a separate screen or link",[127,381,382,385],{},[84,383,384],{},"Core API"," for in-store scenarios where the POS triggers charge\u002Fstatus directly",[193,387,388,419],{},[158,389,392],{"icon":390,"label":391},"i-lucide-credit-card","Checkout Flow",[124,393,394,400,403,406,412],{},[127,395,396,397],{},"Create order in Laravel with status ",[332,398,399],{},"pending",[127,401,402],{},"Request Midtrans transaction token from backend (never expose server keys to React)",[127,404,405],{},"Open Snap modal or charge via Core API depending on payment method",[127,407,408,409],{},"Midtrans webhook hits ",[332,410,411],{},"\u002Fwebhooks\u002Fmidtrans",[127,413,414,415,418],{},"Laravel verifies signature, updates order to ",[332,416,417],{},"paid",", and emits receipt-ready event",[158,420,423],{"icon":421,"label":422},"i-lucide-shield-check","Reliability Rules",[124,424,425,435,438,441],{},[127,426,427,428,335,431,434],{},"Store ",[332,429,430],{},"order_id",[332,432,433],{},"transaction_id",", and raw webhook payload for audit",[127,436,437],{},"Treat webhooks as idempotent duplicate notifications must not double charge stock",[127,439,440],{},"Fail gracefully when payment expires; release reserved inventory",[127,442,443],{},"Log every state transition for end of day reconciliation",[77,445,446],{},[107,447],{"alt":448,"className":449,"height":112,"src":450,"width":114},"Payment terminal with card inserted at checkout",[111],"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1775750276384-123de61b4903?w=1200&h=400&fit=crop",[116,452,454],{"id":453},"bluetooth-thermal-printing-escpos","Bluetooth Thermal Printing (ESC\u002FPOS)",[77,456,457,458,461,462,465],{},"Printing was the hardest part of the stack. Most cheap thermal printers speak ",[84,459,460],{},"ESC\u002FPOS"," over Bluetooth Serial Port Profile (SPP). Browsers do not expose classic Bluetooth the way native apps do so I used the ",[84,463,464],{},"Web Bluetooth API"," where supported.",[77,467,468],{},"The flow:",[230,470,471,474,477,483],{},[127,472,473],{},"Laravel returns a normalized receipt DTO (store name, items, totals, payment method, timestamp)",[127,475,476],{},"A client-side encoder converts that DTO into ESC\u002FPOS byte commands",[127,478,479,482],{},[332,480,481],{},"navigator.bluetooth.requestDevice()"," pairs with the printer",[127,484,485],{},"Bytes stream to the printer characteristic; cashier gets paper in seconds",[193,487,488,512],{},[158,489,492],{"icon":490,"label":491},"i-lucide-printer","Receipt Contents",[124,493,494,497,500,503,506,509],{},[127,495,496],{},"Store header + address",[127,498,499],{},"Order number and cashier name",[127,501,502],{},"Line items with qty × price",[127,504,505],{},"Subtotal, discount, tax, grand total",[127,507,508],{},"Payment method (cash, QRIS, card via Midtrans)",[127,510,511],{},"Footer message and optional QR code",[158,513,516],{"icon":514,"label":515},"i-lucide-alert-triangle","Production Gotchas",[124,517,518,521,524,527],{},[127,519,520],{},"Web Bluetooth works best in Chromium browsers; always provide PDF\u002Femail fallback",[127,522,523],{},"Printer paper width (58mm vs 80mm) changes column layout encode both profiles",[127,525,526],{},"Reconnect logic matters; Bluetooth drops during busy hours",[127,528,529],{},"Test with real hardware early emulator bytes ≠ real printer behavior",[116,531,533],{"id":532},"database-inventory-model","Database & Inventory Model",[77,535,536],{},"A POS lives or dies on inventory accuracy. I modeled:",[124,538,539,545,555,561],{},[127,540,541,544],{},[84,542,543],{},"Products"," with SKU, price tiers, and active\u002Farchived state",[127,546,547,550,551,554],{},[84,548,549],{},"Stock movements"," as ledger entries (sale, restock, adjustment) instead of mutating a single ",[332,552,553],{},"qty"," column blindly",[127,556,557,560],{},[84,558,559],{},"Orders"," with polymorphic payment status and soft deletes for audit trails",[127,562,563,566],{},[84,564,565],{},"Shifts"," (optional) to tie cashiers to sessions and simplify daily closing reports",[77,568,569],{},"Every sale creates an order, order items, and outbound stock movements in one transaction. Refunds create inverse movements rather than deleting history.",[116,571,573],{"id":572},"what-i-would-do-differently","What I Would Do Differently",[77,575,576],{},"Shipping this taught me a few lessons worth sharing:",[124,578,579,585,591,597],{},[127,580,581,584],{},[84,582,583],{},"Start with printer compatibility tests"," before polishing the React UI",[127,586,587,590],{},[84,588,589],{},"Keep the POS offline tolerant"," cache product catalog locally for search when the network flickers",[127,592,593,596],{},[84,594,595],{},"Use queues"," for heavy report generation instead of blocking checkout",[127,598,599,602],{},[84,600,601],{},"Document Midtrans sandbox vs production"," config clearly; webhook URLs differ per environment",[116,604,606],{"id":605},"closing-thoughts","Closing Thoughts",[77,608,609],{},"A modern POS is not one feature it is orchestration. Laravel keeps business logic honest. Inertia.js and React make the cashier experience feel instant. Midtrans handles payments in a market specific way. Bluetooth printing closes the loop with a physical receipt customers expect.",[77,611,612],{},"If you are building something similar, optimize for the cashier first. Every extra click at checkout multiplies across thousands of transactions. Everything else reports, admin panels, integrations can iterate after the core loop is fast and reliable.",[155,614,615,624,633],{},[158,616,617],{},[77,618,619],{},[107,620],{"alt":621,"className":622,"height":112,"src":623,"width":112},"Card reader payment in a retail environment",[111],"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1556742049-0cfed4f6a45d?w=400&h=400&fit=crop",[158,625,626],{},[77,627,628],{},[107,629],{"alt":630,"className":631,"height":112,"src":632,"width":112},"POS system on counter with touchscreen interface",[111],"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1742240216264-f0aac25ef4ba?w=400&h=400&fit=crop",[158,634,635],{},[77,636,637],{},[107,638],{"alt":639,"className":640,"height":112,"src":641,"width":112},"Retail checkout with payment hardware",[111],"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1775750276384-123de61b4903?w=400&h=400&fit=crop",[643,644],"hr",{},[77,646,647],{},[648,649,650,651,658],"em",{},"Images from ",[652,653,657],"a",{"href":654,"rel":655},"https:\u002F\u002Funsplash.com",[656],"nofollow","Unsplash"," — free to use under the Unsplash License.",{"title":296,"searchDepth":660,"depth":660,"links":661},2,[662,663,668,669,670,671,672],{"id":118,"depth":660,"text":119},{"id":187,"depth":660,"text":188,"children":664},[665,667],{"id":256,"depth":666,"text":257},3,{"id":309,"depth":666,"text":310},{"id":364,"depth":660,"text":365},{"id":453,"depth":660,"text":454},{"id":532,"depth":660,"text":533},{"id":572,"depth":660,"text":573},{"id":605,"depth":660,"text":606},"2026-06-18","A full-stack walkthrough of building a production-ready point-of-sale from Laravel APIs and Inertia.js pages to Midtrans payments and ESC\u002FPOS Bluetooth receipt printing.","md",{"src":677},"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1742240216264-f0aac25ef4ba?w=640&h=360&fit=crop",{},true,"\u002Fblog\u002Fbluetooth-pos",{"keywords":682,"title":64,"description":674},"Laravel, React, Inertia.js, POS, Midtrans, Bluetooth printing, ESC\u002FPOS, point of sale","3.blog\u002F1.bluetooth-pos","GmZXGyHku-oVI38azto4ac3urzE7GqMdXdH9jgYxJWI",{"id":686,"title":687,"authors":688,"badge":691,"body":693,"date":673,"description":1062,"extension":675,"image":1063,"meta":1065,"navigation":679,"path":1066,"seo":1067,"stem":1069,"__hash__":1070},"posts\u002F3.blog\u002F2.dev-to-manager.md","10 Things I Had to Unlearn When Moving from Developer to Project Manager",[689],{"name":67,"to":68,"avatar":690},{"src":70},{"label":692},"Leadership, Career",{"type":74,"value":694,"toc":1045},[695,702,705,712,716,719,722,726,729,750,754,757,768,772,775,782,811,815,818,828,832,835,842,867,871,874,881,885,888,891,935,939,946,949,956,960,963,989,991,998,1001,1007,1036,1038],[77,696,697,698,701],{},"The hardest part of becoming a project manager was not learning Jira, writing status reports, or running standups. It was ",[84,699,700],{},"unlearning"," habits that made me a good developer, but actively worked against me as a leader.",[77,703,704],{},"When I moved from shipping features to shipping outcomes through other people, these ten beliefs had to go.",[77,706,707],{},[107,708],{"alt":709,"className":710,"height":112,"src":711,"width":114},"Team collaborating around laptops in a modern workspace",[111],"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1522071820081-009f0129c71c?w=1200&h=400&fit=crop",[116,713,715],{"id":714},"_1-if-i-want-it-done-right-i-should-do-it-myself","1. \"If I want it done right, I should do it myself\"",[77,717,718],{},"As a developer, this mindset saved deadlines. As a PM, it creates bottlenecks, burns you out, and signals distrust to your team.",[77,720,721],{},"Your job is not to be the fastest implementer in the room. It is to remove blockers, clarify priorities, and make sure the right person owns the work. Sometimes that means accepting an 85% solution that ships on time over a 100% solution only you could build three weeks late.",[116,723,725],{"id":724},"_2-more-hours-equals-more-progress","2. \"More hours equals more progress\"",[77,727,728],{},"Developers often equate deep focus time with output. Management runs on a different clock: alignment, communication, and decision latency.",[251,730,731,735,738,741],{},[254,732,734],{"id":733},"the-shift","The shift",[77,736,737],{},"I stopped measuring my day by lines of code or tickets closed. I started measuring whether the team knew what to do next, whether stakeholders had fewer surprises, and whether blockers died within 24 hours.",[77,739,740],{},"Long meetings feel unproductive until you realize one misaligned hour costs the team ten rework hours.",[294,742,743],{"v-slot:image":296},[77,744,745],{},[107,746],{"alt":747,"className":748,"height":112,"src":749,"width":112},"Colleagues discussing a project at a shared desk",[111],"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1600880292203-757bb62b4baf?w=400&h=400&fit=crop",[116,751,753],{"id":752},"_3-silence-means-everything-is-fine","3. \"Silence means everything is fine\"",[77,755,756],{},"In code, no errors often means success. In teams, silence usually means confusion, hesitation, or someone swallowing a concern until it becomes a production incident.",[77,758,759,760,763,764,767],{},"I had to unlearn waiting for problems to surface in standup and start asking direct questions: ",[648,761,762],{},"What is at risk? What are we assuming? Who is blocked?"," Good PMs listen for what is ",[84,765,766],{},"not"," being said.",[116,769,771],{"id":770},"_4-perfect-plans-prevent-failure","4. \"Perfect plans prevent failure\"",[77,773,774],{},"Developers love comprehensive specs. Project managers live in uncertainty. Scope shifts. Vendors slip. Priorities change after an executive email.",[77,776,777,778,781],{},"I unlearned the fantasy of the perfect upfront plan and embraced ",[84,779,780],{},"rolling planning"," enough detail for the next sprint or milestone, with explicit assumptions and a clear re-plan trigger when reality diverges.",[155,783,784,793,802],{},[158,785,786],{},[77,787,788],{},[107,789],{"alt":790,"className":791,"height":112,"src":792,"width":112},"Sticky notes and planning on a glass board",[111],"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1552664730-d307ca884978?w=400&h=400&fit=crop",[158,794,795],{},[77,796,797],{},[107,798],{"alt":799,"className":800,"height":112,"src":801,"width":112},"Team reviewing tasks on a kanban board",[111],"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1611224923853-80b023f02d71?w=400&h=400&fit=crop",[158,803,804],{},[77,805,806],{},[107,807],{"alt":808,"className":809,"height":112,"src":810,"width":112},"Project roadmap sketched on a whiteboard",[111],"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1531403009284-440f080d1e12?w=400&h=400&fit=crop",[116,812,814],{"id":813},"_5-technical-correctness-wins-every-argument","5. \"Technical correctness wins every argument\"",[77,816,817],{},"Being the smartest person about the stack feels good. It does not always move the project forward.",[77,819,820,821,824,825],{},"Stakeholders care about cost, risk, timeline, and user impact, not whether you chose the more elegant abstraction. I learned to translate technical trade-offs into business language: ",[648,822,823],{},"\"This adds two weeks but cuts outage risk.\""," ",[648,826,827],{},"\"We can ship Friday if we defer analytics to phase two.\"",[116,829,831],{"id":830},"_6-saying-yes-keeps-everyone-happy","6. \"Saying yes keeps everyone happy\"",[77,833,834],{},"Developers want to be helpful. PMs who say yes to everything guarantee failure quietly.",[77,836,837,838,841],{},"I had to unlearn people pleasing and get comfortable with ",[84,839,840],{},"structured no's"," no with a reason, no with an alternative, no with a timeline for when yes becomes possible. Protecting the team's capacity is part of the job, not a personality flaw.",[251,843,844,848,851,858],{":reverse":306},[254,845,847],{"id":846},"_7-my-value-is-my-output","7. \"My value is my output\"",[77,849,850],{},"Your calendar will fill with conversations that produce nothing visible in Git. That does not mean the day was wasted.",[77,852,853,854,857],{},"Facilitating a decision, resolving a cross-team dependency, or rewriting a vague requirement so engineering does not build the wrong thing that ",[648,855,856],{},"is"," the output now.",[294,859,860],{"v-slot:image":296},[77,861,862],{},[107,863],{"alt":864,"className":865,"height":112,"src":866,"width":112},"Manager leading a team meeting in a conference room",[111],"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1517245386807-bb43fd082920?w=400&h=400&fit=crop",[116,868,870],{"id":869},"_8-conflict-is-a-sign-something-is-broken","8. \"Conflict is a sign something is broken\"",[77,872,873],{},"In engineering, merge conflicts get resolved and you move on. In teams, healthy conflict is often the first sign people care enough to push back.",[77,875,876,877,880],{},"I unlearned treating disagreement as dysfunction. The goal is not zero conflict it is ",[84,878,879],{},"fast, respectful resolution"," with a documented decision and a committed next step.",[116,882,884],{"id":883},"_9-documentation-is-overhead","9. \"Documentation is overhead\"",[77,886,887],{},"As a developer, docs sometimes felt like a tax on shipping. As a PM, docs are how you scale yourself.",[77,889,890],{},"Decision logs, RACI clarity, meeting notes with action owners, and written scope boundaries prevent the same questions from eating the same hours every week. If it is not written down, it will be relitigated.",[193,892,893,914],{},[158,894,897],{"icon":895,"label":896},"i-lucide-file-text","Worth Writing Down",[124,898,899,902,905,908,911],{},[127,900,901],{},"Scope in \u002F scope out for each milestone",[127,903,904],{},"Decisions with date, owner, and rationale",[127,906,907],{},"Risk register with mitigation owners",[127,909,910],{},"Dependencies on other teams or vendors",[127,912,913],{},"Definition of done agreed with engineering",[158,915,918],{"icon":916,"label":917},"i-lucide-message-circle","Worth Saying Out Loud",[124,919,920,923,929,932],{},[127,921,922],{},"What changed since last week and why",[127,924,925,926,928],{},"What we are ",[84,927,766],{}," doing this sprint",[127,930,931],{},"Who owns the final call when opinions split",[127,933,934],{},"When the team should escalate vs. decide locally",[116,936,938],{"id":937},"_10-i-need-to-have-all-the-answers","10. \"I need to have all the answers\"",[77,940,941,942,945],{},"Developers are rewarded for solving problems. PMs are rewarded for ",[84,943,944],{},"framing problems"," so the right people can solve them.",[77,947,948],{},"I unlearned the pressure to instantly know the answer in every meeting. \"I do not know yet I will confirm by Thursday and update the channel\" is a valid, professional response. Credibility comes from follow through, not from bluffing.",[77,950,951],{},[107,952],{"alt":953,"className":954,"height":112,"src":955,"width":114},"Professional team planning session with notebooks and laptops",[111],"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1556767547-0a0255a3b90a?w=1200&h=400&fit=crop",[116,957,959],{"id":958},"what-actually-helped-me-transition","What Actually Helped Me Transition",[77,961,962],{},"Unlearning is only half the story. These habits accelerated the shift:",[124,964,965,971,977,983],{},[127,966,967,970],{},[84,968,969],{},"Stay technically literate"," not to code every feature, but to spot unrealistic estimates and respect engineering constraints",[127,972,973,976],{},[84,974,975],{},"Default to over communication early"," in a project; you can always reduce noise later",[127,978,979,982],{},[84,980,981],{},"Protect maker time"," for your team the way you once protected your own",[127,984,985,988],{},[84,986,987],{},"Ask engineers what they need from you"," the answer is rarely \"more meetings\"",[116,990,606],{"id":605},[77,992,993,994,997],{},"Moving from developer to project manager is not a promotion away from craft. It is a promotion into ",[84,995,996],{},"multiplier work"," your leverage comes through people, clarity, and judgment.",[77,999,1000],{},"The best PMs I know still think like builders. They just unlearned the instinct to build everything alone.",[77,1002,1003,1004,1006],{},"If you are making this transition now: be patient with yourself. You are not becoming less technical. You are learning when ",[84,1005,766],{}," to be the one typing.",[155,1008,1009,1018,1027],{},[158,1010,1011],{},[77,1012,1013],{},[107,1014],{"alt":1015,"className":1016,"height":112,"src":1017,"width":112},"Handshake between colleagues after a successful meeting",[111],"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1521791136064-7986c2920216?w=400&h=400&fit=crop",[158,1019,1020],{},[77,1021,1022],{},[107,1023],{"alt":1024,"className":1025,"height":112,"src":1026,"width":112},"Team celebrating progress together in the office",[111],"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1528605248644-14dd04022da1?w=400&h=400&fit=crop",[158,1028,1029],{},[77,1030,1031],{},[107,1032],{"alt":1033,"className":1034,"height":112,"src":1035,"width":112},"Focused work session with laptop and coffee",[111],"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1497366216548-37526070297c?w=400&h=400&fit=crop",[643,1037],{},[77,1039,1040],{},[648,1041,650,1042,658],{},[652,1043,657],{"href":654,"rel":1044},[656],{"title":296,"searchDepth":660,"depth":660,"links":1046},[1047,1048,1051,1052,1053,1054,1057,1058,1059,1060,1061],{"id":714,"depth":660,"text":715},{"id":724,"depth":660,"text":725,"children":1049},[1050],{"id":733,"depth":666,"text":734},{"id":752,"depth":660,"text":753},{"id":770,"depth":660,"text":771},{"id":813,"depth":660,"text":814},{"id":830,"depth":660,"text":831,"children":1055},[1056],{"id":846,"depth":666,"text":847},{"id":869,"depth":660,"text":870},{"id":883,"depth":660,"text":884},{"id":937,"depth":660,"text":938},{"id":958,"depth":660,"text":959},{"id":605,"depth":660,"text":606},"The mental shifts that matter most when you stop writing code full-time and start leading people, timelines, and trade-offs instead.",{"src":1064},"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1522071820081-009f0129c71c?w=640&h=360&fit=crop",{},"\u002Fblog\u002Fdev-to-manager",{"keywords":1068,"title":687,"description":1062},"project management, developer to manager, leadership, career, software team, engineering management","3.blog\u002F2.dev-to-manager","nnaoM_Z5VZcR2UGHGvgs2xinUy4I50vImowV4PijlQ4",{"id":1072,"title":1073,"authors":1074,"badge":1077,"body":1079,"date":1798,"description":1799,"extension":675,"image":1800,"meta":1802,"navigation":679,"path":1803,"seo":1804,"stem":1808,"__hash__":1809},"posts\u002F3.blog\u002F3.the-hidden-technical-debt-in-laravel-projects.md","The Hidden Technical Debt in Laravel Projects",[1075],{"name":67,"to":68,"avatar":1076},{"src":70},{"label":1078},"Laravel, Architecture",{"type":74,"value":1080,"toc":1785},[1081,1087,1094,1101,1108,1112,1115,1141,1144,1148,1154,1244,1247,1267,1270,1283,1287,1305,1322,1325,1351,1368,1374,1378,1385,1388,1399,1425,1436,1439,1443,1461,1468,1471,1511,1522,1525,1547,1551,1554,1624,1639,1642,1646,1653,1685,1688,1692,1735,1739,1746,1749,1756,1772,1774,1781],[77,1082,1083,1086],{},[84,1084,1085],{},"Laravel technical debt"," rarely announces itself. The first six months feel effortless — routes are expressive, Eloquent reads like English, and features ship fast. Then something shifts. A simple bug fix touches four files. A new developer asks where business logic lives and nobody gives the same answer. Deployments get slower. Tests become brittle.",[77,1088,1089,1090,1093],{},"If you have maintained a Laravel application past its second year, you have seen this pattern. The framework did not fail you — ",[84,1091,1092],{},"accumulated architectural decisions"," did. Laravel maintainability problems are rarely visible in a single pull request. They hide in fat controllers, inconsistent service layers, premature repository abstractions, and modular folders that share everything except real boundaries.",[77,1095,1096,1097,1100],{},"This is why most Laravel projects become difficult to maintain — not overnight, but through small shortcuts that compound into ",[84,1098,1099],{},"codebase maintenance"," debt you pay on every sprint.",[77,1102,1103],{},[107,1104],{"alt":1105,"className":1106,"height":112,"src":1107,"width":114},"Laravel developer reviewing legacy PHP code for technical debt on a laptop",[111],"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1555066931-4365d14bab8c?w=1200&h=400&fit=crop",[116,1109,1111],{"id":1110},"why-maintenance-gets-harder-over-time","Why Maintenance Gets Harder Over Time",[77,1113,1114],{},"Most Laravel projects do not collapse from one bad decision. They erode from dozens of small, reasonable ones:",[124,1116,1117,1123,1129,1135],{},[127,1118,1119,1122],{},[84,1120,1121],{},"Velocity pressure"," — shipping beats refactoring when deadlines loom",[127,1124,1125,1128],{},[84,1126,1127],{},"Inconsistent patterns"," — one feature uses a service class, the next puts everything in the controller",[127,1130,1131,1134],{},[84,1132,1133],{},"Leaky boundaries"," — models, jobs, and listeners all reach into the database differently",[127,1136,1137,1140],{},[84,1138,1139],{},"Copy-paste growth"," — validation, authorization, and query logic duplicated across layers",[77,1142,1143],{},"The result is a codebase that still \"works\" but resists change. Experienced developers feel it immediately: high cognitive load, fear of regressions, and refactors that never get prioritized because nobody can estimate them safely.",[116,1145,1147],{"id":1146},"fat-controllers-where-laravel-technical-debt-usually-starts","Fat Controllers: Where Laravel Technical Debt Usually Starts",[77,1149,1150,1153],{},[84,1151,1152],{},"Fat controllers"," are the most common entry point for Laravel technical debt — and often where it never leaves. Thin controllers are a well-documented best practice; ignoring them is usually the first architectural shortcut teams regret.",[1155,1156,1160],"pre",{"className":1157,"code":1158,"language":1159,"meta":296,"style":296},"language-php shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","public function store(Request $request)\n{\n    $validated = $request->validate([...]);\n\n    $user = User::where('email', $validated['email'])->first();\n    if ($user && $user->subscription?->isActive()) {\n        \u002F\u002F 40 more lines of billing, notification, and audit logic\n    }\n\n    Mail::to($admin)->send(new NewSignupMail($user));\n    event(new UserRegistered($user));\n\n    return redirect()->route('dashboard');\n}\n","php",[332,1161,1162,1170,1175,1180,1186,1192,1198,1204,1210,1215,1221,1227,1232,1238],{"__ignoreMap":296},[1163,1164,1167],"span",{"class":1165,"line":1166},"line",1,[1163,1168,1169],{},"public function store(Request $request)\n",[1163,1171,1172],{"class":1165,"line":660},[1163,1173,1174],{},"{\n",[1163,1176,1177],{"class":1165,"line":666},[1163,1178,1179],{},"    $validated = $request->validate([...]);\n",[1163,1181,1183],{"class":1165,"line":1182},4,[1163,1184,1185],{"emptyLinePlaceholder":679},"\n",[1163,1187,1189],{"class":1165,"line":1188},5,[1163,1190,1191],{},"    $user = User::where('email', $validated['email'])->first();\n",[1163,1193,1195],{"class":1165,"line":1194},6,[1163,1196,1197],{},"    if ($user && $user->subscription?->isActive()) {\n",[1163,1199,1201],{"class":1165,"line":1200},7,[1163,1202,1203],{},"        \u002F\u002F 40 more lines of billing, notification, and audit logic\n",[1163,1205,1207],{"class":1165,"line":1206},8,[1163,1208,1209],{},"    }\n",[1163,1211,1213],{"class":1165,"line":1212},9,[1163,1214,1185],{"emptyLinePlaceholder":679},[1163,1216,1218],{"class":1165,"line":1217},10,[1163,1219,1220],{},"    Mail::to($admin)->send(new NewSignupMail($user));\n",[1163,1222,1224],{"class":1165,"line":1223},11,[1163,1225,1226],{},"    event(new UserRegistered($user));\n",[1163,1228,1230],{"class":1165,"line":1229},12,[1163,1231,1185],{"emptyLinePlaceholder":679},[1163,1233,1235],{"class":1165,"line":1234},13,[1163,1236,1237],{},"    return redirect()->route('dashboard');\n",[1163,1239,1241],{"class":1165,"line":1240},14,[1163,1242,1243],{},"}\n",[77,1245,1246],{},"When controllers own validation, authorization, querying, business rules, side effects, and response formatting, every endpoint becomes a mini-application. The problems compound quietly:",[124,1248,1249,1255,1261],{},[127,1250,1251,1254],{},[84,1252,1253],{},"Testing"," requires HTTP fakes or full integration tests for logic that has nothing to do with routing",[127,1256,1257,1260],{},[84,1258,1259],{},"Reuse"," forces other developers to extract methods from controllers or duplicate logic in jobs and commands",[127,1262,1263,1266],{},[84,1264,1265],{},"Onboarding"," means new team members must read entire controller files to understand one behavior",[77,1268,1269],{},"Laravel encourages thin controllers by design. The framework gives you Form Requests, Policies, Events, and Jobs precisely so HTTP entry points stay thin. Ignoring that separation is not a Laravel limitation — it is the first layer of hidden debt.",[1271,1272,1273],"callout",{},[77,1274,1275,1278,1279,1282],{},[84,1276,1277],{},"A practical rule:"," If a controller method exceeds one screen of code or contains an ",[332,1280,1281],{},"if"," branch that encodes business policy, that logic belongs elsewhere.",[116,1284,1286],{"id":1285},"laravel-service-layer-helpful-abstraction-or-extra-indirection","Laravel Service Layer: Helpful Abstraction or Extra Indirection?",[77,1288,1289,1290,1293,1294,335,1297,1300,1301,1304],{},"The natural reaction to fat controllers is a ",[84,1291,1292],{},"Laravel service layer"," — a ",[332,1295,1296],{},"UserRegistrationService",[332,1298,1299],{},"OrderService",", or ",[332,1302,1303],{},"BillingService"," that controllers delegate to.",[77,1306,1307,1308,1311,1312,335,1315,1300,1318,1321],{},"Used well, services extract orchestration: coordinate models, dispatch events, call external APIs, and keep controllers to a few lines. Used poorly, they become ",[84,1309,1310],{},"glorified procedure scripts"," — 300-line classes with static dependencies, no interfaces, and methods named ",[332,1313,1314],{},"handle()",[332,1316,1317],{},"process()",[332,1319,1320],{},"doStuff()",".",[77,1323,1324],{},"Common service-layer debt patterns:",[124,1326,1327,1333,1339,1345],{},[127,1328,1329,1332],{},[84,1330,1331],{},"God services"," — one class per domain area that knows everything and changes for every feature",[127,1334,1335,1338],{},[84,1336,1337],{},"Anemic services"," — thin wrappers around Eloquent calls that add a namespace but no real behavior",[127,1340,1341,1344],{},[84,1342,1343],{},"Inconsistent placement"," — some logic in services, some in models, some still in controllers",[127,1346,1347,1350],{},[84,1348,1349],{},"Constructor injection sprawl"," — services depending on five other services, making unit tests expensive",[77,1352,1353,1354,1357,1358,335,1361,335,1364,1367],{},"Services shine when they represent a ",[84,1355,1356],{},"use case"," with a clear boundary: ",[332,1359,1360],{},"RegisterUser",[332,1362,1363],{},"ProcessRefund",[332,1365,1366],{},"SyncInventoryFromWarehouse",". They fail when they are a dumping ground for \"anything that is not a controller.\"",[77,1369,1370,1371],{},"The experienced developer's question is not \"should we use services?\" but ",[84,1372,1373],{},"\"what is the unit of behavior we are extracting, and who owns it?\"",[116,1375,1377],{"id":1376},"laravel-repository-pattern-solving-a-problem-you-might-not-have","Laravel Repository Pattern: Solving a Problem You Might Not Have",[77,1379,1380,1381,1384],{},"The ",[84,1382,1383],{},"Laravel repository pattern"," promises clean data access: controllers and services talk to interfaces, implementations swap freely, and Eloquent stays behind a wall.",[77,1386,1387],{},"In practice, repository patterns in Laravel often create debt instead of removing it.",[77,1389,1390,1391,1394,1395,1398],{},"Eloquent already is a repository and unit-of-work abstraction. Wrapping ",[332,1392,1393],{},"User::where(...)->get()"," in ",[332,1396,1397],{},"EloquentUserRepository::findByEmail()"," frequently adds:",[124,1400,1401,1407,1413,1419],{},[127,1402,1403,1406],{},[84,1404,1405],{},"Boilerplate"," — interface + implementation for every model",[127,1408,1409,1412],{},[84,1410,1411],{},"Leaky abstractions"," — repositories returning Eloquent models or collections, so callers still depend on the ORM",[127,1414,1415,1418],{},[84,1416,1417],{},"False testability wins"," — mocking repositories while integration bugs live in query scopes and relationships",[127,1420,1421,1424],{},[84,1422,1423],{},"Query fragmentation"," — business-critical queries scattered across repositories, models, and raw DB calls",[77,1426,1427,1428,1431,1432,1435],{},"Repositories earn their place when you have a genuine need to ",[84,1429,1430],{},"swap storage backends",", enforce strict persistence boundaries in a large team, or isolate complex query composition behind a stable contract. For most Laravel apps, ",[84,1433,1434],{},"well-designed models, query scopes, and dedicated query objects"," solve the same problem with less ceremony.",[77,1437,1438],{},"Hidden debt here is ideological: teams adopt repositories because a blog post said \"real architecture uses repositories,\" not because their app has a persistence problem worth that cost.",[116,1440,1442],{"id":1441},"laravel-modular-architecture-structure-without-discipline-still-fails","Laravel Modular Architecture: Structure Without Discipline Still Fails",[77,1444,1445,1446,335,1449,1452,1453,1456,1457,1460],{},"As apps grow, folders multiply: ",[332,1447,1448],{},"Modules\u002FBilling",[332,1450,1451],{},"Domains\u002FInventory",", package-style boundaries via ",[332,1454,1455],{},"nwidart\u002Flaravel-modules"," or custom PSR-4 roots. ",[84,1458,1459],{},"Laravel modular architecture"," promises independent teams, clearer ownership, and replaceable subsystems.",[77,1462,1463,1464,1467],{},"Without discipline, modules become ",[84,1465,1466],{},"mini-monoliths"," that share the same database, the same global helpers, and the same event bus with no contracts.",[77,1469,1470],{},"Signs modular debt is accumulating:",[124,1472,1473,1479,1485,1495,1505],{},[127,1474,1475,1478],{},[84,1476,1477],{},"Circular dependencies"," — Billing imports Inventory internals; Inventory fires Billing events that call back",[127,1480,1481,1484],{},[84,1482,1483],{},"Shared Eloquent models"," — every module reads every table; schema changes require archaeology",[127,1486,1487,1490,1491,1494],{},[84,1488,1489],{},"Leaked configuration"," — ",[332,1492,1493],{},".env"," keys and config files referenced across module boundaries",[127,1496,1497,1500,1501,1504],{},[84,1498,1499],{},"Duplicate concepts"," — two modules each define their own ",[332,1502,1503],{},"Status"," enum, DTO, or \"shared\" utility folder",[127,1506,1507,1510],{},[84,1508,1509],{},"Integration tests as the only safety net"," — because unit boundaries were never real",[77,1512,1513,1514,1517,1518,1521],{},"True modularity requires ",[84,1515,1516],{},"explicit public APIs"," per module: DTOs in, DTOs out, internal models hidden, events documented, and migrations owned by one module. Laravel does not enforce this. Composer autoloading makes it easy to ",[332,1519,1520],{},"use"," anything from anywhere — which is both a feature and a trap.",[77,1523,1524],{},"Modular architecture pays off at organizational scale. Below that, it often adds navigation overhead without reducing coupling. The debt appears when structure is adopted for aesthetics rather than enforced boundaries.",[251,1526,1527,1531,1538],{},[254,1528,1530],{"id":1529},"the-coupling-test","The coupling test",[77,1532,1533,1534,1537],{},"Before splitting into modules, ask: ",[648,1535,1536],{},"If I delete this folder, how many unrelated tests break?"," If the answer is \"most of them,\" you have folders — not modules.",[294,1539,1540],{"v-slot:image":296},[77,1541,1542],{},[107,1543],{"alt":1544,"className":1545,"height":112,"src":1546,"width":112},"Software architecture diagram sketched on a whiteboard",[111],"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1531403009284-441f980d9e7e?w=400&h=400&fit=crop",[116,1548,1550],{"id":1549},"how-the-layers-interact-and-where-debt-compounds","How the Layers Interact (and Where Debt Compounds)",[77,1552,1553],{},"These four topics are not independent choices. They stack:",[1555,1556,1557,1573],"table",{},[1558,1559,1560],"thead",{},[1561,1562,1563,1567,1570],"tr",{},[1564,1565,1566],"th",{},"Symptom",[1564,1568,1569],{},"Common cause",[1564,1571,1572],{},"What actually helps",[1574,1575,1576,1588,1599,1610],"tbody",{},[1561,1577,1578,1582,1585],{},[1579,1580,1581],"td",{},"200-line controller",[1579,1583,1584],{},"No extraction discipline",[1579,1586,1587],{},"Form Requests, Actions, or focused use-case classes",[1561,1589,1590,1593,1596],{},[1579,1591,1592],{},"15 service classes, unclear ownership",[1579,1594,1595],{},"Services as catch-all",[1579,1597,1598],{},"Name by use case; colocate with domain",[1561,1600,1601,1604,1607],{},[1579,1602,1603],{},"Repository per model, still coupled to Eloquent",[1579,1605,1606],{},"Pattern for pattern's sake",[1579,1608,1609],{},"Scopes, custom builders, query objects",[1561,1611,1612,1618,1621],{},[1579,1613,1614,1617],{},[332,1615,1616],{},"Modules\u002F"," tree, shared models everywhere",[1579,1619,1620],{},"Structural modularity only",[1579,1622,1623],{},"Module APIs, anti-corruption layers",[77,1625,1626,1627,1630,1631,1634,1635,1638],{},"The worst Laravel codebases I have inherited had ",[84,1628,1629],{},"all four patterns implemented inconsistently",". Fat controllers next to thin ones. Repositories beside raw Eloquent. A ",[332,1632,1633],{},"Services"," folder with 40 classes and a ",[332,1636,1637],{},"Modules"," folder nobody fully understands.",[77,1640,1641],{},"Consistency matters more than which pattern you pick.",[116,1643,1645],{"id":1644},"laravel-refactoring-paying-down-technical-debt-without-a-rewrite","Laravel Refactoring: Paying Down Technical Debt Without a Rewrite",[77,1647,1648,1649,1652],{},"Full rewrites are rarely justified. ",[84,1650,1651],{},"Laravel refactoring"," works best as incremental, bounded change — not a big-bang rewrite nobody can estimate. Experienced teams chip away with:",[230,1654,1655,1661,1667,1673,1679],{},[127,1656,1657,1660],{},[84,1658,1659],{},"Identify hot paths"," — the controllers and jobs that change every sprint. Extract those first.",[127,1662,1663,1666],{},[84,1664,1665],{},"Pick one orchestration style"," — Actions, services, or invokable classes. Document it in a short ADR and enforce it in review.",[127,1668,1669,1672],{},[84,1670,1671],{},"Stop adding repositories"," unless persistence is genuinely swappable. Prefer explicit query objects for complex reads.",[127,1674,1675,1678],{},[84,1676,1677],{},"Draw module boundaries on paper"," before creating folders. Define what is public and what is internal.",[127,1680,1681,1684],{},[84,1682,1683],{},"Write characterization tests"," before moving logic — not to hit coverage targets, but to lock behavior before refactors.",[77,1686,1687],{},"Laravel's ecosystem gives you the tools: Form Requests, Policies, Events, Jobs, Actions (via packages or single-purpose classes), and Pint for keeping style consistent. The framework does not choose your architecture. It rewards clarity when you finally commit to one.",[116,1689,1691],{"id":1690},"frequently-asked-questions","Frequently Asked Questions",[1693,1694,1695,1702,1717,1723,1729],"accordion",{},[1696,1697,1699],"accordion-item",{"label":1698},"What are the main signs of Laravel technical debt?",[77,1700,1701],{},"Slow feature delivery, fear of touching legacy controllers, inconsistent patterns across modules, rising bug counts after small changes, and difficulty onboarding new developers. If upgrading Laravel or PHP feels risky, debt has likely compounded in your architecture — not your framework version.",[1696,1703,1705],{"label":1704},"Should I use a service layer in every Laravel project?",[77,1706,1707,1708,335,1710,1712,1713,1716],{},"No. A Laravel service layer helps when you need clear use-case orchestration — registration, checkout, sync jobs. It hurts when it becomes a catch-all folder for logic nobody knew where else to put. Name services by behavior (",[332,1709,1360],{},[332,1711,1363],{},"), not by entity alone (",[332,1714,1715],{},"UserService",").",[1696,1718,1720],{"label":1719},"Is the repository pattern recommended for Laravel?",[77,1721,1722],{},"Only when you have a real persistence boundary to enforce — multiple storage backends, strict team contracts, or complex query composition worth isolating. For most apps, Eloquent models with scopes, custom builders, and query objects deliver better Laravel maintainability with less boilerplate than a repository per model.",[1696,1724,1726],{"label":1725},"When does modular architecture make sense in Laravel?",[77,1727,1728],{},"When teams are large enough to own separate domains, when module APIs are enforced, and when shared database access is deliberate — not accidental. Folders alone do not create modularity. If deleting one module breaks unrelated tests, you have structural debt, not modular architecture.",[1696,1730,1732],{"label":1731},"How do I refactor a Laravel codebase without stopping feature work?",[77,1733,1734],{},"Start with hot paths that change every sprint. Add characterization tests, extract one use case at a time, document your orchestration style in a short ADR, and enforce it in code review. Incremental Laravel refactoring on high-churn areas beats a six-month rewrite every time.",[116,1736,1738],{"id":1737},"conclusion","Conclusion",[77,1740,1741,1742,1745],{},"Most Laravel projects become difficult to maintain not because the framework ages poorly, but because ",[84,1743,1744],{},"technical debt hides inside familiar patterns",". Fat controllers feel productive in week one. Service layers feel professional in month three. Repository patterns feel enterprise-grade in month six. Modular architecture feels scalable in year one.",[77,1747,1748],{},"Each pattern can improve Laravel maintainability. Each can also make a codebase harder to change. The difference is whether you applied it to solve a real coupling problem — or to avoid naming boundaries and sticking to them.",[77,1750,1751,1752,1755],{},"If you are years into a Laravel codebase that feels heavier than it should, start with one question: ",[648,1753,1754],{},"Where does this behavior live, and why?"," Answer it consistently across your team. That is how hidden debt becomes visible — and finally payable.",[1271,1757,1758],{},[77,1759,1760,1763,1764,1767,1768,1771],{},[84,1761,1762],{},"Building or untangling a Laravel application?"," I write about real-world Laravel architecture — from ",[652,1765,1766],{"href":680},"POS systems with Inertia.js and React"," to the structural decisions that determine whether a project scales or stalls. If your team is hitting maintainability walls, hit ",[84,1769,1770],{},"Connect"," on this site — I help teams refactor incrementally without betting the product on a rewrite.",[643,1773],{},[77,1775,1776],{},[648,1777,650,1778,658],{},[652,1779,657],{"href":654,"rel":1780},[656],[1782,1783,1784],"style",{},"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":296,"searchDepth":660,"depth":660,"links":1786},[1787,1788,1789,1790,1791,1794,1795,1796,1797],{"id":1110,"depth":660,"text":1111},{"id":1146,"depth":660,"text":1147},{"id":1285,"depth":660,"text":1286},{"id":1376,"depth":660,"text":1377},{"id":1441,"depth":660,"text":1442,"children":1792},[1793],{"id":1529,"depth":666,"text":1530},{"id":1549,"depth":660,"text":1550},{"id":1644,"depth":660,"text":1645},{"id":1690,"depth":660,"text":1691},{"id":1737,"depth":660,"text":1738},"2026-06-25","Why most Laravel codebases become difficult to maintain — and how fat controllers, service layers, repository patterns, and modular architecture quietly compound the problem.",{"src":1801},"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1555066931-4365d14bab8c?w=640&h=360&fit=crop",{},"\u002Fblog\u002Fthe-hidden-technical-debt-in-laravel-projects",{"title":1805,"description":1806,"keywords":1807},"Laravel Technical Debt — Why Projects Become Hard to Maintain","Signs of Laravel technical debt in fat controllers, service layers, repository patterns, and modular architecture — plus practical refactoring strategies for long-term maintainability.","Laravel, technical debt, architecture, fat controllers, service layer, repository pattern, refactoring, maintainability","3.blog\u002F3.the-hidden-technical-debt-in-laravel-projects","JizXfyhrYTO748z4Mzn2RXo6R2zC15UsAFWII7Uf8rk",{"id":1811,"title":1812,"authors":1813,"badge":1816,"body":1818,"date":2844,"description":2845,"extension":675,"image":2846,"meta":2848,"navigation":679,"path":2849,"seo":2850,"stem":2854,"__hash__":2855},"posts\u002F3.blog\u002F4.7-docker-mistakes-i-keep-seeing-on-production-servers.md","7 Docker Mistakes I Keep Seeing on Production Servers",[1814],{"name":67,"to":68,"avatar":1815},{"src":70},{"label":1817},"Docker, DevOps, VPS",{"type":74,"value":1819,"toc":2828},[1820,1830,1837,1843,1850,1854,1857,1864,1868,1874,1877,1913,1916,1921,1952,1960,1964,1971,1974,1978,1984,2064,2071,2078,2082,2089,2092,2096,2099,2147,2150,2177,2188,2192,2195,2198,2215,2226,2230,2255,2273,2277,2292,2295,2324,2328,2331,2407,2414,2418,2425,2431,2435,2559,2566,2574,2581,2584,2601,2605,2635,2639,2745,2747,2787,2789,2795,2798,2801,2816,2818,2825],[77,1821,1822,1823,1826,1827],{},"Docker on a VPS feels like free infrastructure. One ",[332,1824,1825],{},"docker compose up -d"," and your app is live. No Kubernetes. No platform team. Just you, a $12 server, and a README that says ",[648,1828,1829],{},"\"it works on my machine.\"",[77,1831,1832,1833,1836],{},"Then production teaches the lesson. Disk hits 100%. SSH becomes unresponsive. ",[332,1834,1835],{},"docker ps"," shows forty containers and half of them are from projects you forgot existed. The OOM killer takes out MySQL because a Node process leaked memory for three weeks. You discover backups were never configured — only hoped for.",[77,1838,1839,1842],{},[84,1840,1841],{},"Docker production mistakes"," do not always look dramatic. They look like small defaults left unchanged until the VPS crashes at 2 a.m. These are the seven I see most often on real servers — and what to do instead.",[77,1844,1845],{},[107,1846],{"alt":1847,"className":1848,"height":112,"src":1849,"width":114},"Server room with blinking lights and rack infrastructure",[111],"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1558494949-ef010cbdcc31?w=1200&h=400&fit=crop",[116,1851,1853],{"id":1852},"why-vps-docker-is-a-different-beast","Why VPS + Docker Is a Different Beast",[77,1855,1856],{},"A VPS is not a managed platform. There is no autoscaler, no built-in log drain, no volume snapshot button you cannot forget to click. You own the full stack: kernel, disk, networking, TLS, and every container you ever started and never removed.",[77,1858,1859,1860,1863],{},"That is why ",[84,1861,1862],{},"Docker DevOps"," on a single server punishes shortcuts harder than local development ever will. The mistakes below compound quietly — until they do not.",[116,1865,1867],{"id":1866},"_1-container-sprawl","1. Container Sprawl",[77,1869,1870,1873],{},[84,1871,1872],{},"Container sprawl"," is what happens when every experiment, client project, and \"quick staging\" environment lives on the same VPS forever.",[77,1875,1876],{},"Typical symptoms:",[124,1878,1879,1895,1898,1910],{},[127,1880,1881,1882,1885,1886,335,1889,345,1892],{},"Twelve ",[332,1883,1884],{},"docker-compose.yml"," files across ",[332,1887,1888],{},"\u002Fopt",[332,1890,1891],{},"\u002Fhome\u002Fdeploy",[332,1893,1894],{},"\u002Fvar\u002Fwww",[127,1896,1897],{},"Stopped containers from 2024 still holding port bindings and volume mounts",[127,1899,1900,1901,335,1904,335,1907],{},"Duplicate databases — ",[332,1902,1903],{},"postgres",[332,1905,1906],{},"postgres-staging",[332,1908,1909],{},"postgres-old",[127,1911,1912],{},"Nobody knows which stack serves which domain",[77,1914,1915],{},"Sprawl wastes RAM and disk, creates port conflicts, and makes incident response slow. During an outage, you are not debugging one app — you are archaeology.",[77,1917,1918],{},[84,1919,1920],{},"Fix it:",[124,1922,1923,1932,1939,1945],{},[127,1924,1925,1926,335,1929],{},"One directory per environment: ",[332,1927,1928],{},"\u002Fsrv\u002Fappname\u002Fproduction",[332,1930,1931],{},"\u002Fsrv\u002Fappname\u002Fstaging",[127,1933,1934,1935,1938],{},"Run ",[332,1936,1937],{},"docker system prune"," on a schedule — but only after confirming nothing important is stopped",[127,1940,1941,1942],{},"Use explicit project names: ",[332,1943,1944],{},"docker compose -p myapp-prod up -d",[127,1946,1947,1948,1951],{},"Document what runs where. A simple ",[332,1949,1950],{},"SERVICES.md"," on the server beats tribal knowledge",[1271,1953,1954],{},[77,1955,1956,1959],{},[84,1957,1958],{},"Rule of thumb:"," If you cannot list every running container and its purpose in under two minutes, you have sprawl.",[116,1961,1963],{"id":1962},"_2-logging-with-no-rotation","2. Logging With No Rotation",[77,1965,1966,1967,1970],{},"Docker captures stdout\u002Fstderr into JSON log files on the host — by default, ",[84,1968,1969],{},"with no size limit",". On a busy Laravel, Node, or nginx container, those files grow until the root partition is full.",[77,1972,1973],{},"Then everything fails at once: database writes, SSL renewals, SSH sessions, deploy scripts.",[77,1975,1976],{},[84,1977,1920],{},[77,1979,1980,1981,1983],{},"Set log rotation in ",[332,1982,1884],{}," or your daemon config:",[1155,1985,1989],{"className":1986,"code":1987,"language":1988,"meta":296,"style":296},"language-yaml shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","services:\n  app:\n    logging:\n      driver: json-file\n      options:\n        max-size: \"10m\"\n        max-file: \"5\"\n","yaml",[332,1990,1991,2001,2008,2015,2027,2034,2050],{"__ignoreMap":296},[1163,1992,1993,1997],{"class":1165,"line":1166},[1163,1994,1996],{"class":1995},"swJcz","services",[1163,1998,2000],{"class":1999},"sMK4o",":\n",[1163,2002,2003,2006],{"class":1165,"line":660},[1163,2004,2005],{"class":1995},"  app",[1163,2007,2000],{"class":1999},[1163,2009,2010,2013],{"class":1165,"line":666},[1163,2011,2012],{"class":1995},"    logging",[1163,2014,2000],{"class":1999},[1163,2016,2017,2020,2023],{"class":1165,"line":1182},[1163,2018,2019],{"class":1995},"      driver",[1163,2021,2022],{"class":1999},":",[1163,2024,2026],{"class":2025},"sfazB"," json-file\n",[1163,2028,2029,2032],{"class":1165,"line":1188},[1163,2030,2031],{"class":1995},"      options",[1163,2033,2000],{"class":1999},[1163,2035,2036,2039,2041,2044,2047],{"class":1165,"line":1194},[1163,2037,2038],{"class":1995},"        max-size",[1163,2040,2022],{"class":1999},[1163,2042,2043],{"class":1999}," \"",[1163,2045,2046],{"class":2025},"10m",[1163,2048,2049],{"class":1999},"\"\n",[1163,2051,2052,2055,2057,2059,2062],{"class":1165,"line":1200},[1163,2053,2054],{"class":1995},"        max-file",[1163,2056,2022],{"class":1999},[1163,2058,2043],{"class":1999},[1163,2060,2061],{"class":2025},"5",[1163,2063,2049],{"class":1999},[77,2065,2066,2067,2070],{},"For production at scale, ship logs to a central sink — Loki, Grafana Cloud, Papertrail, or at minimum rsyslog. ",[84,2068,2069],{},"Docker logging"," on a VPS is not \"set and forget\" unless you cap it.",[77,2072,2073,2074,2077],{},"Also watch application-level logs mounted to volumes. A Laravel ",[332,2075,2076],{},"storage\u002Flogs"," directory on a bind mount can fill disk just as fast as Docker's json-file driver.",[116,2079,2081],{"id":2080},"_3-ignoring-memory-leaks","3. Ignoring Memory Leaks",[77,2083,2084,2085,2088],{},"Applications leak memory. Node workers, PHP-FPM pools, Java heaps, Redis misconfigurations — they all drift upward over days. Without limits, one leaky container consumes the VPS until the ",[84,2086,2087],{},"Linux OOM killer"," intervenes.",[77,2090,2091],{},"The OOM killer does not politely stop your leak. It picks a process — often MySQL, PostgreSQL, or dockerd itself — and terminates it. Your \"random\" database crash at 3 a.m. was probably a memory leak three containers away.",[77,2093,2094],{},[84,2095,1920],{},[77,2097,2098],{},"Set memory limits per service:",[1155,2100,2102],{"className":1986,"code":2101,"language":1988,"meta":296,"style":296},"services:\n  app:\n    deploy:\n      resources:\n        limits:\n          memory: 512M\n",[332,2103,2104,2110,2116,2123,2130,2137],{"__ignoreMap":296},[1163,2105,2106,2108],{"class":1165,"line":1166},[1163,2107,1996],{"class":1995},[1163,2109,2000],{"class":1999},[1163,2111,2112,2114],{"class":1165,"line":660},[1163,2113,2005],{"class":1995},[1163,2115,2000],{"class":1999},[1163,2117,2118,2121],{"class":1165,"line":666},[1163,2119,2120],{"class":1995},"    deploy",[1163,2122,2000],{"class":1999},[1163,2124,2125,2128],{"class":1165,"line":1182},[1163,2126,2127],{"class":1995},"      resources",[1163,2129,2000],{"class":1999},[1163,2131,2132,2135],{"class":1165,"line":1188},[1163,2133,2134],{"class":1995},"        limits",[1163,2136,2000],{"class":1999},[1163,2138,2139,2142,2144],{"class":1165,"line":1194},[1163,2140,2141],{"class":1995},"          memory",[1163,2143,2022],{"class":1999},[1163,2145,2146],{"class":2025}," 512M\n",[77,2148,2149],{},"On standalone Docker Compose (non-Swarm), use the older syntax that still works in practice:",[1155,2151,2153],{"className":1986,"code":2152,"language":1988,"meta":296,"style":296},"services:\n  app:\n    mem_limit: 512m\n",[332,2154,2155,2161,2167],{"__ignoreMap":296},[1163,2156,2157,2159],{"class":1165,"line":1166},[1163,2158,1996],{"class":1995},[1163,2160,2000],{"class":1999},[1163,2162,2163,2165],{"class":1165,"line":660},[1163,2164,2005],{"class":1995},[1163,2166,2000],{"class":1999},[1163,2168,2169,2172,2174],{"class":1165,"line":666},[1163,2170,2171],{"class":1995},"    mem_limit",[1163,2173,2022],{"class":1999},[1163,2175,2176],{"class":2025}," 512m\n",[77,2178,2179,2180,2183,2184,2187],{},"Monitor with ",[332,2181,2182],{},"docker stats"," or Prometheus + cAdvisor. Restart policies help containers recover, but they do not fix leaks — they just delay the crash. Profile the app, cap workers, and tune pool sizes (PHP-FPM ",[332,2185,2186],{},"pm.max_children",", Node cluster size, etc.).",[116,2189,2191],{"id":2190},"_4-no-backup-strategy-for-volumes","4. No Backup Strategy for Volumes",[77,2193,2194],{},"This is the mistake that only hurts once — when you need a restore and discover backups were never real.",[77,2196,2197],{},"Common failures:",[124,2199,2200,2206,2209,2212],{},[127,2201,2202,2203],{},"Data stored inside the container filesystem instead of a ",[84,2204,2205],{},"named volume",[127,2207,2208],{},"Volumes exist but nothing snapshots them",[127,2210,2211],{},"Backups copy the git repo, not the PostgreSQL data directory",[127,2213,2214],{},"\"I will set up backups next week\" for eleven months",[77,2216,2217,2218,2221,2222,2225],{},"A ",[332,2219,2220],{},"docker compose down"," or ",[332,2223,2224],{},"docker volume rm"," on the wrong day becomes permanent data loss.",[77,2227,2228],{},[84,2229,1920],{},[124,2231,2232,2235,2245,2252],{},[127,2233,2234],{},"Always use named volumes for databases and uploaded files",[127,2236,2237,2238,335,2241,2244],{},"Automate backups: ",[332,2239,2240],{},"pg_dump",[332,2242,2243],{},"mysqldump",", or volume snapshots via your provider (Hetzner, DigitalOcean, Vultr all support this)",[127,2246,2247,2248,2251],{},"Store backups ",[84,2249,2250],{},"off-server"," — S3, Backblaze B2, another region",[127,2253,2254],{},"Test restores quarterly. A backup you have never restored is a guess",[251,2256,2257,2261,2264],{},[254,2258,2260],{"id":2259},"the-3-2-1-rule-still-applies","The 3-2-1 rule still applies",[77,2262,2263],{},"Three copies, two media types, one off-site. Docker does not change that — it just makes it easier to forget where your data actually lives.",[294,2265,2266],{"v-slot:image":296},[77,2267,2268],{},[107,2269],{"alt":2270,"className":2271,"height":112,"src":2272,"width":112},"External hard drives stacked for backup redundancy",[111],"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1597872200969-2b65d56bd16b?w=400&h=400&fit=crop",[116,2274,2276],{"id":2275},"_5-skipping-or-misconfiguring-reverse-proxies","5. Skipping or Misconfiguring Reverse Proxies",[77,2278,2279,2280,2283,2284,2287,2288,2291],{},"Exposing ",[332,2281,2282],{},"app:3000"," directly on ",[332,2285,2286],{},"0.0.0.0:3000"," is the fastest way to turn a VPS into a public attack surface. ",[84,2289,2290],{},"Reverse proxies"," — Traefik, Caddy, nginx — exist to terminate TLS, route by hostname, rate-limit, and keep application ports off the public internet.",[77,2293,2294],{},"Mistakes I see constantly:",[124,2296,2297,2304,2307,2310,2317],{},[127,2298,2299,2300,2303],{},"Apps bound to ",[332,2301,2302],{},"0.0.0.0"," with no firewall, no proxy, no TLS",[127,2305,2306],{},"Manual certbot on nginx configs that break on renewal",[127,2308,2309],{},"Mixing proxy configs across three tools on one server",[127,2311,2312,2313,2316],{},"WebSocket and SSE apps proxied without proper ",[332,2314,2315],{},"Upgrade"," headers",[127,2318,2319,2320,2323],{},"Trusting ",[332,2321,2322],{},"X-Forwarded-For"," without setting real IP headers correctly",[77,2325,2326],{},[84,2327,1920],{},[77,2329,2330],{},"Pick one reverse proxy per server. Traefik or Caddy shine with Docker labels; nginx is fine if you already know it cold.",[1155,2332,2334],{"className":1986,"code":2333,"language":1988,"meta":296,"style":296},"services:\n  app:\n    expose:\n      - \"3000\"\n    labels:\n      - \"traefik.enable=true\"\n      - \"traefik.http.routers.app.rule=Host(`app.example.com`)\"\n      - \"traefik.http.routers.app.tls.certresolver=letsencrypt\"\n",[332,2335,2336,2342,2348,2355,2367,2374,2385,2396],{"__ignoreMap":296},[1163,2337,2338,2340],{"class":1165,"line":1166},[1163,2339,1996],{"class":1995},[1163,2341,2000],{"class":1999},[1163,2343,2344,2346],{"class":1165,"line":660},[1163,2345,2005],{"class":1995},[1163,2347,2000],{"class":1999},[1163,2349,2350,2353],{"class":1165,"line":666},[1163,2351,2352],{"class":1995},"    expose",[1163,2354,2000],{"class":1999},[1163,2356,2357,2360,2362,2365],{"class":1165,"line":1182},[1163,2358,2359],{"class":1999},"      -",[1163,2361,2043],{"class":1999},[1163,2363,2364],{"class":2025},"3000",[1163,2366,2049],{"class":1999},[1163,2368,2369,2372],{"class":1165,"line":1188},[1163,2370,2371],{"class":1995},"    labels",[1163,2373,2000],{"class":1999},[1163,2375,2376,2378,2380,2383],{"class":1165,"line":1194},[1163,2377,2359],{"class":1999},[1163,2379,2043],{"class":1999},[1163,2381,2382],{"class":2025},"traefik.enable=true",[1163,2384,2049],{"class":1999},[1163,2386,2387,2389,2391,2394],{"class":1165,"line":1200},[1163,2388,2359],{"class":1999},[1163,2390,2043],{"class":1999},[1163,2392,2393],{"class":2025},"traefik.http.routers.app.rule=Host(`app.example.com`)",[1163,2395,2049],{"class":1999},[1163,2397,2398,2400,2402,2405],{"class":1165,"line":1206},[1163,2399,2359],{"class":1999},[1163,2401,2043],{"class":1999},[1163,2403,2404],{"class":2025},"traefik.http.routers.app.tls.certresolver=letsencrypt",[1163,2406,2049],{"class":1999},[77,2408,2409,2410,2413],{},"Bind apps to internal networks only. Let the proxy be the single public entry point on ports 80 and 443. Lock everything else with ",[332,2411,2412],{},"ufw"," or your provider firewall.",[116,2415,2417],{"id":2416},"_6-no-health-checks-or-restart-policies","6. No Health Checks or Restart Policies",[77,2419,2420,2421,2424],{},"Containers exit. Processes segfault. Servers reboot. Without ",[332,2422,2423],{},"restart: unless-stopped"," and proper health checks, your app stays down until a human notices — or until a client sends an angry email.",[77,2426,2427,2428,2430],{},"I have SSH'd into servers where ",[332,2429,1835],{}," showed a clean bill of health, but the app container had been restarting in a crash loop for days because nothing monitored it.",[77,2432,2433],{},[84,2434,1920],{},[1155,2436,2438],{"className":1986,"code":2437,"language":1988,"meta":296,"style":296},"services:\n  app:\n    restart: unless-stopped\n    healthcheck:\n      test: [\"CMD\", \"curl\", \"-f\", \"http:\u002F\u002Flocalhost:3000\u002Fhealth\"]\n      interval: 30s\n      timeout: 5s\n      retries: 3\n      start_period: 40s\n",[332,2439,2440,2446,2452,2462,2469,2518,2528,2538,2549],{"__ignoreMap":296},[1163,2441,2442,2444],{"class":1165,"line":1166},[1163,2443,1996],{"class":1995},[1163,2445,2000],{"class":1999},[1163,2447,2448,2450],{"class":1165,"line":660},[1163,2449,2005],{"class":1995},[1163,2451,2000],{"class":1999},[1163,2453,2454,2457,2459],{"class":1165,"line":666},[1163,2455,2456],{"class":1995},"    restart",[1163,2458,2022],{"class":1999},[1163,2460,2461],{"class":2025}," unless-stopped\n",[1163,2463,2464,2467],{"class":1165,"line":1182},[1163,2465,2466],{"class":1995},"    healthcheck",[1163,2468,2000],{"class":1999},[1163,2470,2471,2474,2476,2479,2482,2485,2487,2490,2492,2495,2497,2499,2501,2504,2506,2508,2510,2513,2515],{"class":1165,"line":1188},[1163,2472,2473],{"class":1995},"      test",[1163,2475,2022],{"class":1999},[1163,2477,2478],{"class":1999}," [",[1163,2480,2481],{"class":1999},"\"",[1163,2483,2484],{"class":2025},"CMD",[1163,2486,2481],{"class":1999},[1163,2488,2489],{"class":1999},",",[1163,2491,2043],{"class":1999},[1163,2493,2494],{"class":2025},"curl",[1163,2496,2481],{"class":1999},[1163,2498,2489],{"class":1999},[1163,2500,2043],{"class":1999},[1163,2502,2503],{"class":2025},"-f",[1163,2505,2481],{"class":1999},[1163,2507,2489],{"class":1999},[1163,2509,2043],{"class":1999},[1163,2511,2512],{"class":2025},"http:\u002F\u002Flocalhost:3000\u002Fhealth",[1163,2514,2481],{"class":1999},[1163,2516,2517],{"class":1999},"]\n",[1163,2519,2520,2523,2525],{"class":1165,"line":1194},[1163,2521,2522],{"class":1995},"      interval",[1163,2524,2022],{"class":1999},[1163,2526,2527],{"class":2025}," 30s\n",[1163,2529,2530,2533,2535],{"class":1165,"line":1200},[1163,2531,2532],{"class":1995},"      timeout",[1163,2534,2022],{"class":1999},[1163,2536,2537],{"class":2025}," 5s\n",[1163,2539,2540,2543,2545],{"class":1165,"line":1206},[1163,2541,2542],{"class":1995},"      retries",[1163,2544,2022],{"class":1999},[1163,2546,2548],{"class":2547},"sbssI"," 3\n",[1163,2550,2551,2554,2556],{"class":1165,"line":1212},[1163,2552,2553],{"class":1995},"      start_period",[1163,2555,2022],{"class":1999},[1163,2557,2558],{"class":2025}," 40s\n",[77,2560,2561,2562,2565],{},"Expose a real ",[332,2563,2564],{},"\u002Fhealth"," endpoint — not just \"port open.\" Check database connectivity if your app needs it. Pair this with uptime monitoring (Uptime Kuma, Better Stack, Pingdom) so failures reach you before users do.",[116,2567,2569,2570,2573],{"id":2568},"_7-treating-latest-and-dev-defaults-as-production-config","7. Treating ",[332,2571,2572],{},"latest"," and Dev Defaults as Production Config",[77,2575,2576,2577,2580],{},"Pulling ",[332,2578,2579],{},"image: latest"," on every deploy is not continuous delivery — it is roulette. A upstream breaking change at midnight becomes your outage.",[77,2582,2583],{},"Other dev defaults that leak into production:",[124,2585,2586,2592,2595,2598],{},[127,2587,2588,2591],{},[332,2589,2590],{},"DEBUG=true"," in environment variables",[127,2593,2594],{},"Compose files with volume mounts to source code (hot reload on a VPS)",[127,2596,2597],{},"No pinned image digests or version tags",[127,2599,2600],{},"Docker socket mounted into containers for \"convenience\"",[77,2602,2603],{},[84,2604,1920],{},[124,2606,2607,2616,2625,2632],{},[127,2608,2609,2610,335,2613],{},"Pin versions: ",[332,2611,2612],{},"postgres:16.2",[332,2614,2615],{},"redis:7.2-alpine",[127,2617,2618,2619,2622,2623],{},"Use ",[332,2620,2621],{},".env.production"," separate from local ",[332,2624,1493],{},[127,2626,2627,2628,2631],{},"Never mount ",[332,2629,2630],{},"\u002Fvar\u002Frun\u002Fdocker.sock"," unless you fully understand the security implications",[127,2633,2634],{},"Tag and push your own app images to a registry — build once, deploy the same artifact",[116,2636,2638],{"id":2637},"quick-reference-symptom-likely-cause","Quick Reference: Symptom → Likely Cause",[1555,2640,2641,2653],{},[1558,2642,2643],{},[1561,2644,2645,2647,2650],{},[1564,2646,1566],{},[1564,2648,2649],{},"Likely cause",[1564,2651,2652],{},"First move",[1574,2654,2655,2674,2691,2713,2724,2735],{},[1561,2656,2657,2660,2663],{},[1579,2658,2659],{},"Disk full, SSH sluggish",[1579,2661,2662],{},"Unbounded Docker logs",[1579,2664,2665,2666,2669,2670,2673],{},"Set ",[332,2667,2668],{},"max-size"," \u002F ",[332,2671,2672],{},"max-file","; prune old logs",[1561,2675,2676,2679,2682],{},[1579,2677,2678],{},"Random DB restarts",[1579,2680,2681],{},"OOM from memory leak",[1579,2683,2684,2686,2687,2690],{},[332,2685,2182],{},"; set ",[332,2688,2689],{},"mem_limit","; fix the leak",[1561,2692,2693,2696,2703],{},[1579,2694,2695],{},"\"It worked yesterday\" after reboot",[1579,2697,2698,2699,2702],{},"No ",[332,2700,2701],{},"restart"," policy",[1579,2704,2705,2706,2709,2710],{},"Add ",[332,2707,2708],{},"unless-stopped","; check ",[332,2711,2712],{},"docker ps -a",[1561,2714,2715,2718,2721],{},[1579,2716,2717],{},"SSL expired",[1579,2719,2720],{},"Proxy\u002Fcertbot misconfig",[1579,2722,2723],{},"Centralize TLS on one reverse proxy",[1561,2725,2726,2729,2732],{},[1579,2727,2728],{},"Data gone after redeploy",[1579,2730,2731],{},"No volume \u002F no backup",[1579,2733,2734],{},"Named volumes + off-site dumps",[1561,2736,2737,2740,2742],{},[1579,2738,2739],{},"Too many unknown containers",[1579,2741,1872],{},[1579,2743,2744],{},"Audit, name projects, prune, document",[116,2746,1691],{"id":1690},[1693,2748,2749,2755,2761,2772],{},[1696,2750,2752],{"label":2751},"How much RAM does Docker need on a VPS?",[77,2753,2754],{},"Plan for your apps plus 20–30% headroom for the OS, Docker daemon, and spikes. A small stack (reverse proxy + app + database) often needs 2 GB minimum; 4 GB is safer for production. Always set per-container memory limits so one service cannot take the whole server.",[1696,2756,2758],{"label":2757},"What is the best reverse proxy for Docker on a VPS?",[77,2759,2760],{},"Traefik and Caddy integrate cleanly with Docker labels and automatic Let's Encrypt. nginx is excellent if you prefer explicit config files. The best choice is the one you will maintain consistently — not three different proxies on the same box.",[1696,2762,2764],{"label":2763},"How do I back up Docker volumes on a VPS?",[77,2765,2766,2767,335,2769,2771],{},"Use database-native dumps (",[332,2768,2240],{},[332,2770,2243],{},") on a schedule, copy artifacts off-server, and optionally snapshot volumes at the provider level. Test restores regularly. Backups that only exist on the same VPS as your data are not disaster recovery.",[1696,2773,2775],{"label":2774},"Why does my VPS crash after running Docker for weeks?",[77,2776,2777,2778,335,2781,345,2784,2786],{},"Usually disk full from logs, memory exhaustion from leaks without limits, or container sprawl consuming resources. Check ",[332,2779,2780],{},"df -h",[332,2782,2783],{},"docker system df",[332,2785,2182],{},". Those three commands diagnose most \"mystery\" VPS crashes.",[116,2788,1738],{"id":1737},[77,2790,2791,2794],{},[84,2792,2793],{},"Docker on a production VPS"," rewards discipline more than cleverness. Container sprawl, runaway logging, unchecked memory leaks, missing backup strategy, and reverse proxies done wrong are not exotic failures — they are the default outcome when Docker is treated as a local dev tool dropped onto a live server.",[77,2796,2797],{},"None of these require Kubernetes or an enterprise platform. They require caps on logs, limits on memory, named volumes with real backups, one reverse proxy at the edge, and the habit of cleaning up what you deploy.",[77,2799,2800],{},"Your future self — the one SSH'ing in at 2 a.m. — will thank you for boring infrastructure.",[1271,2802,2803],{},[77,2804,2805,2808,2809,2812,2813,2815],{},[84,2806,2807],{},"Running Docker on a VPS and not sure if your setup will survive traffic?"," I write about production architecture from ",[652,2810,2811],{"href":680},"Laravel stacks"," to the ops layer that keeps them online. If your server keeps crashing and you want a second pair of eyes on compose files, logging, and backups — hit ",[84,2814,1770],{}," on this site.",[643,2817],{},[77,2819,2820],{},[648,2821,650,2822,658],{},[652,2823,657],{"href":654,"rel":2824},[656],[1782,2826,2827],{},"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 .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);}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"title":296,"searchDepth":660,"depth":660,"links":2829},[2830,2831,2832,2833,2834,2837,2838,2839,2841,2842,2843],{"id":1852,"depth":660,"text":1853},{"id":1866,"depth":660,"text":1867},{"id":1962,"depth":660,"text":1963},{"id":2080,"depth":660,"text":2081},{"id":2190,"depth":660,"text":2191,"children":2835},[2836],{"id":2259,"depth":666,"text":2260},{"id":2275,"depth":660,"text":2276},{"id":2416,"depth":660,"text":2417},{"id":2568,"depth":660,"text":2840},"7. Treating latest and Dev Defaults as Production Config",{"id":2637,"depth":660,"text":2638},{"id":1690,"depth":660,"text":1691},{"id":1737,"depth":660,"text":1738},"2026-06-26","Container sprawl, runaway logging, memory leaks, missing backups, and reverse proxy misconfigurations — the Docker mistakes that crash VPS servers and how to avoid them.",{"src":2847},"https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1558494949-ef010cbdcc31?w=640&h=360&fit=crop",{},"\u002Fblog\u002F7-docker-mistakes-i-keep-seeing-on-production-servers",{"title":2851,"description":2852,"keywords":2853},"7 Docker Mistakes That Crash VPS Servers — Production DevOps Guide","Common Docker production mistakes on VPS servers — container sprawl, logging, memory leaks, backup strategy, and reverse proxies — plus fixes that keep your stack online.","Docker, DevOps, VPS, production servers, container sprawl, logging, memory leaks, reverse proxy, backups","3.blog\u002F4.7-docker-mistakes-i-keep-seeing-on-production-servers","2mgPBszBNoMKGnFi6UeJAgn-WvWrJxPtWS59oMl-w3c",{"id":2857,"title":2858,"authors":2859,"badge":2862,"body":2864,"date":3002,"description":3003,"extension":675,"image":3004,"meta":3005,"navigation":679,"path":3006,"seo":3007,"stem":3011,"__hash__":3012},"posts\u002F3.blog\u002F5.whatsapp-username-what-is-it-and-what-should-we-do.md","WhatsApp Username, What Is It and What Should We Do?",[2860],{"name":67,"to":68,"avatar":2861},{"src":70},{"label":2863},"WhatsApp, Privacy",{"type":74,"value":2865,"toc":2994},[2866,2869,2881,2885,2888,2895,2899,2902,2906,2909,2913,2916,2923,2927,2981,2985],[77,2867,2868],{},"WhatsApp usernames are trending because WhatsApp officially began letting users reserve usernames before a broader launch later in 2026. The feature is meant to let people start chats without immediately sharing their phone number, which is a major shift because WhatsApp has traditionally been phone-number-first.",[2870,2871,2875,2876],"video",{"controls":679,"className":2872,"width":114,"height":112,"poster":2874},[111,2873],"w-full","https:\u002F\u002Fabout.fb.com\u002Fwp-content\u002Fuploads\u002F2026\u002F06\u002FIts-time-to-reserve-your-WhatsApp-username_Header.jpg?w=1200","\n  ",[2877,2878],"source",{"src":2879,"type":2880},"https:\u002F\u002Fabout.fb.com\u002Fwp-content\u002Fuploads\u002F2026\u002F06\u002FWhatsApp-Usernames-LaunchFilm.mp4","video\u002Fmp4",[116,2882,2884],{"id":2883},"how-whatsapp-usernames-work","How WhatsApp Usernames Work",[77,2886,2887],{},"A WhatsApp username will work like a unique handle, shown with an @ symbol. WhatsApp says there will be no public username directory and no suggestions, so someone must know your exact username to contact you. WhatsApp is also adding an optional username key, which adds another layer of control before someone can message you.",[77,2889,2890,2891,2894],{},"People can reserve a username from the latest mobile app through ",[84,2892,2893],{},"Settings > Account > Username",", though availability is rolling out gradually by country. WhatsApp's Help Center also says username reservation is not currently available on WhatsApp Web or Desktop, so users should use their primary phone.",[116,2896,2898],{"id":2897},"the-privacy-upside","The Privacy Upside",[77,2900,2901],{},"The positive side is privacy: users, creators, businesses, and community members can connect without exposing a personal phone number to every new contact. This is especially useful for group chats, events, online selling, networking, and customer communication.",[116,2903,2905],{"id":2904},"the-controversy","The Controversy",[77,2907,2908],{},"The controversy is about impersonation and scams. Reuters reported that India asked WhatsApp to pause the rollout in the country pending consultations, citing risks such as fraud, phishing, digital arrest scams, and usernames that imitate people, banks, or government agencies. Meta said the feature is not fully live yet and that some usernames for public figures, government entities, and verified Meta accounts have been reserved to reduce impersonation.",[116,2910,2912],{"id":2911},"what-people-should-do-now","What People Should Do Now",[77,2914,2915],{},"What people should do now: reserve a clean username early, but avoid using sensitive personal details like your full birth date, ID-like numbers, or anything easy to impersonate. Turn on the username key when available, be suspicious of accounts claiming to be banks, officials, brands, or public figures, and verify important contacts through another trusted channel before sending money, OTPs, documents, or private information.",[77,2917,2918],{},[107,2919],{"alt":2920,"className":2921,"height":112,"src":2922,"width":114},"Setting up a username key in WhatsApp — contact me by username set to People who know my key",[111],"https:\u002F\u002Fmedia.idownloadblog.com\u002Fwp-content\u002Fuploads\u002F2026\u002F06\u002FSetting-up-Username-key-for-my-WhatsApp-account.png",[116,2924,2926],{"id":2925},"pros-and-cons","Pros and Cons",[1555,2928,2929,2939],{},[1558,2930,2931],{},[1561,2932,2933,2936],{},[1564,2934,2935],{},"Pros",[1564,2937,2938],{},"Cons \u002F Risks",[1574,2940,2941,2949,2957,2965,2973],{},[1561,2942,2943,2946],{},[1579,2944,2945],{},"Keeps your phone number private when starting new chats.",[1579,2947,2948],{},"Scammers may impersonate brands, officials, or people with similar-looking usernames.",[1561,2950,2951,2954],{},[1579,2952,2953],{},"Easier for creators, sellers, communities, and businesses to share a memorable handle.",[1579,2955,2956],{},"Popular names may be claimed quickly, causing username competition.",[1561,2958,2959,2962],{},[1579,2960,2961],{},"No searchable public directory, which reduces random discovery.",[1579,2963,2964],{},"If you share your username publicly, unwanted messages may still increase.",[1561,2966,2967,2970],{},[1579,2968,2969],{},"Optional username key gives extra contact control.",[1579,2971,2972],{},"Users may trust a familiar-looking username too quickly.",[1561,2974,2975,2978],{},[1579,2976,2977],{},"Can make WhatsApp feel more flexible, like other handle-based messaging apps.",[1579,2979,2980],{},"Businesses and CRMs that rely on phone numbers may need workflow changes.",[116,2982,2984],{"id":2983},"reference","Reference",[124,2986,2987],{},[127,2988,2989],{},[652,2990,2993],{"href":2991,"rel":2992},"https:\u002F\u002Fabout.fb.com\u002Fnews\u002F2026\u002F06\u002Fits-time-to-reserve-your-whatsapp-username\u002F",[656],"It's Time to Reserve Your WhatsApp Username — Meta Newsroom",{"title":296,"searchDepth":660,"depth":660,"links":2995},[2996,2997,2998,2999,3000,3001],{"id":2883,"depth":660,"text":2884},{"id":2897,"depth":660,"text":2898},{"id":2904,"depth":660,"text":2905},{"id":2911,"depth":660,"text":2912},{"id":2925,"depth":660,"text":2926},{"id":2983,"depth":660,"text":2984},"2026-07-02","WhatsApp usernames let you start chats without sharing your phone number. Here is what the feature means, the privacy benefits, the scam risks, and what to do now.",{"src":2874},{},"\u002Fblog\u002Fwhatsapp-username-what-is-it-and-what-should-we-do",{"title":3008,"description":3009,"keywords":3010},"WhatsApp Username — What Is It and What Should We Do?","A summary of WhatsApp usernames — how reservation works, privacy benefits, impersonation risks, and practical steps to take before the feature launches.","WhatsApp username, fitur username whatsapp, wa username, impersonation, username key, whatsapp username feature","3.blog\u002F5.whatsapp-username-what-is-it-and-what-should-we-do","SURXibMk3M74uwjFQFQKI__JjCoBtzJw59z14XvOY90",{"url":3014},"https:\u002F\u002Fadislksn.web.id",1783705973856]