<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Saimul Islam</title>
    <description>The latest articles on DEV Community by Saimul Islam (@saimulislamdl).</description>
    <link>https://dev.to/saimulislamdl</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2102508%2F731a7a40-a68a-4ee1-8318-0d1d14988552.png</url>
      <title>DEV Community: Saimul Islam</title>
      <link>https://dev.to/saimulislamdl</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/saimulislamdl"/>
    <language>en</language>
    <item>
      <title>10 Essential Tools I Actually Use to Keep My Side Projects From Falling Over</title>
      <dc:creator>Saimul Islam</dc:creator>
      <pubDate>Sun, 06 Sep 2026 12:47:56 +0000</pubDate>
      <link>https://dev.to/saimulislamdl/10-essential-tools-i-actually-use-to-keep-my-side-projects-from-falling-over-1751</link>
      <guid>https://dev.to/saimulislamdl/10-essential-tools-i-actually-use-to-keep-my-side-projects-from-falling-over-1751</guid>
      <description>&lt;p&gt;&lt;em&gt;Docker management, monitoring that goes deeper than a green dot, backups I have actually restored, and everything else that showed up once deploying stopped being the hard part.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Moving off Vercel solved exactly one problem: deploying. Everything else I used to get for free, quietly, as part of the platform, I now had to go find and wire up myself. A month into running my own server, I had a list of ten tools taped to the inside of my head, each one solving a problem I did not know I had until it happened to me at a bad time.&lt;/p&gt;

&lt;p&gt;This is that list, in the order I actually needed them, with the mistake or the moment that made me install each one. I lean JS and Rust wherever I can, partly out of preference and partly because those are the tools that keep pace with how fast the rest of my stack moves. A couple of these are not JS or Rust at all, and I kept them anyway because they were simply the best tool for the job.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Dokploy, for everything I wrote about yesterday
&lt;/h2&gt;

&lt;p&gt;This is the one I already spent an entire post on, so I will keep it short here. Push to main, Dokploy builds the container, Traefik points a domain at it, done. Four apps running on one $24 droplet, and adding a fifth would not move the bill. If you deploy anything with Docker and are still doing it over SSH, start here. Everything else on this list assumes you already have a platform under you, not just a server.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Neon, for the database half of preview environments
&lt;/h2&gt;

&lt;p&gt;The first crack after Dokploy was previews. Dokploy gives every pull request its own preview URL, which is one of the nicest things about the whole setup, right up until every preview hits the same production database. I corrupted a batch of test data twice before I noticed what was happening. Neon branches Postgres the way git branches code, copy-on-write, so a preview PR gets its own preview database that costs almost nothing until it actually diverges from main. The storage engine underneath is written in Rust, and it quietly closed the other half of preview environments that nobody's setup guide mentions.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. OpenObserve, for monitoring that actually explains itself
&lt;/h2&gt;

&lt;p&gt;Dokploy's own dashboard will tell you a container is using too much memory. It will not tell you why, which request caused it, or whether it has happened before. I moved to OpenObserve for that: logs, metrics, and traces in one place, shipped as a single Rust binary, one docker-compose file to run it. The first afternoon I had it wired up, I found a query that had been running slow for three weeks, in the trace waterfall, in about four minutes. My old approach to that same investigation was grep and vibes.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. restic, for the backups Dokploy does not know about
&lt;/h2&gt;

&lt;p&gt;Dokploy's built-in backups cover Postgres, and I use them. They do not cover the config folder for my API, a SQLite file one of my smaller tools writes to, or a volume attached to a service that is not a database at all. restic backs up anything, encrypts it before it leaves the box, deduplicates so a nightly backup of a mostly-unchanged folder costs almost nothing in storage, and restores with one command I have run on purpose more than once, just to check.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;restic &lt;span class="nt"&gt;-r&lt;/span&gt; s3:https://&amp;lt;account&amp;gt;.r2.cloudflarestorage.com/backups backup /var/lib/myapp
restic &lt;span class="nt"&gt;-r&lt;/span&gt; s3:https://&amp;lt;account&amp;gt;.r2.cloudflarestorage.com/backups restore latest &lt;span class="nt"&gt;--target&lt;/span&gt; /tmp/restore-test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It ships as a single static binary, which after living through more dependency hell than I would like to admit, I have come to treat as a feature in its own right.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Sentry, for the bugs logs make you scroll past
&lt;/h2&gt;

&lt;p&gt;Logs tell you something broke. They do not tell you it broke for the same handful of users, three times a day, on the same line of code, until you have already lost interest in scrolling through them. Sentry groups the exception, attaches the stack trace, and tells me exactly which release introduced it. The self-hosted stack is heavier than I would like, but the JS SDK alone, dropped into a Next.js app, has caught more real bugs before a user reported them than anything else on this list.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. OpenReplay, for the tickets that just say “it's broken”
&lt;/h2&gt;

&lt;p&gt;A user once emailed to say a form “just doesn't work,” with no other detail, which used to mean opening dev tools and hoping I could reproduce it. OpenReplay records the session and replays it back like a video, console errors and network requests included, exactly as the user hit them. It is open source and self-hosted, and it has turned half-day investigations into two-minute videos more times than I can count.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Appium, for testing on an actual device instead of a hunch
&lt;/h2&gt;

&lt;p&gt;Once a project had a real mobile wrapper around it, testing it by hand on my one physical phone stopped being a strategy. Appium drives real devices and simulators the same way Selenium drives a browser, through the WebDriver protocol, so the same testing habits carry over. I script the login flow and the checkout flow on both iOS and Android before every release, and it has caught two platform-specific bugs that a browser would never have shown me.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. k6, for finding the ceiling before traffic does
&lt;/h2&gt;

&lt;p&gt;A newsletter mention once doubled my traffic for six hours. The API held, barely, and I had no idea by how much margin. k6 lets me write load tests in JavaScript, the same language as the thing being tested, and run them against staging before a launch instead of finding the ceiling live.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;k6/http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;vus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2m&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://staging.myapp.com/api/health&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ten minutes to write that tells you more than any amount of guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Infisical, for replacing a very bad system
&lt;/h2&gt;

&lt;p&gt;My “secrets management” before this was environment variables pasted into Dokploy's UI and, if I am honest, a couple of .env files sitting in a private Slack channel. Infisical is open source, written in TypeScript, and gives every project versioned secrets, per-environment values, and an audit log of who changed what. Rotating a leaked key used to mean a search-and-replace across three servers. Now it is one dashboard and a redeploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. n8n, for the glue that runs everything else
&lt;/h2&gt;

&lt;p&gt;n8n is the part that quietly holds the other nine together. When OpenObserve crosses an alert threshold, when a restic backup fails, when a new signup needs a welcome sequence, n8n is the thing that notices and does something about it, wired together as a workflow instead of a script I will forget I wrote. It runs on Node, it is free to self-host, and it is the tool I have extended the most since installing it, because every new problem seems to end with “I should probably wire that into n8n.”&lt;/p&gt;




&lt;h3&gt;
  
  
  So, is this too many tools for a side project?
&lt;/h3&gt;

&lt;p&gt;Maybe. Ten tools running on one box is still one box, and the more of these you add, the more you are choosing to own instead of rent. What changed for me is that each one solves a problem I actually had, in the order I actually had it, rather than a checklist I installed on day one and mostly ignored. If you are one platform migration into doing this yourself, start with whichever one of these already hurts.&lt;/p&gt;




&lt;h3&gt;
  
  
  Sources and further reading
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dokploy.com" rel="noopener noreferrer"&gt;Dokploy documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://neon.tech/docs/introduction/branching" rel="noopener noreferrer"&gt;Neon docs: database branching&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://openobserve.ai/docs/" rel="noopener noreferrer"&gt;OpenObserve documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://restic.net" rel="noopener noreferrer"&gt;restic documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://develop.sentry.dev/self-hosted/" rel="noopener noreferrer"&gt;Sentry self-hosted&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.openreplay.com" rel="noopener noreferrer"&gt;OpenReplay documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://appium.io/docs/en/latest/" rel="noopener noreferrer"&gt;Appium documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://k6.io/docs/" rel="noopener noreferrer"&gt;k6 documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://infisical.com/docs" rel="noopener noreferrer"&gt;Infisical documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.n8n.io" rel="noopener noreferrer"&gt;n8n documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>webdev</category>
      <category>rust</category>
    </item>
    <item>
      <title>Stop Paying for Vercel: How to Self-Host Your Deployments on a VPS and Keep the Same Workflow</title>
      <dc:creator>Saimul Islam</dc:creator>
      <pubDate>Fri, 04 Sep 2026 05:19:58 +0000</pubDate>
      <link>https://dev.to/saimulislamdl/stop-paying-for-vercel-how-to-self-host-your-deployments-on-a-vps-and-keep-the-same-workflow-4cce</link>
      <guid>https://dev.to/saimulislamdl/stop-paying-for-vercel-how-to-self-host-your-deployments-on-a-vps-and-keep-the-same-workflow-4cce</guid>
      <description>&lt;p&gt;Push-to-deploy, automatic HTTPS, and preview environments on a $24 droplet with Dokploy, plus the four mistakes to skip on the way.&lt;/p&gt;

&lt;p&gt;For two years, deploying was the easiest part of my week. I pushed to main, closed my laptop, and the site updated itself. I never thought about servers, which was the entire point of paying for it.&lt;/p&gt;

&lt;p&gt;Then I opened the invoice.&lt;/p&gt;

&lt;p&gt;$198 for the month. I read it twice, because the number did not match what I was running: a Next.js frontend with a few thousand monthly visitors, a small API moving JSON around, one Postgres database holding a couple hundred megabytes, and a nightly cron job. Four small things, and I knew that launching a fifth would push the bill up again.&lt;/p&gt;

&lt;p&gt;Here is what is worth understanding before you decide whether any of this applies to you. You are not paying managed platforms for compute. Compute is cheap. You are paying because somebody else already set up the reverse proxy, the TLS certificates that renew themselves, and the pipeline that turns a git push into a running container. That is real work, and it was worth every cent for two years. So the question got narrower: is it still worth it, now that I know which of those features I actually use?&lt;/p&gt;

&lt;p&gt;What follows is the whole setup, end to end: installing Dokploy, connecting your Git provider, getting deploys on every push, domains with automatic HTTPS, object storage for user files, scheduled backups, and monitoring. The workflow you land on is the one you already have. You push to main, you close your laptop, and the site updates itself.&lt;/p&gt;

&lt;p&gt;It took me a weekend, and I have left all four of my mistakes in, because those are the parts no documentation warns you about. If you can ship an application but have never really run a server, this is written for you.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why My First Attempt at This Failed
&lt;/h2&gt;

&lt;p&gt;My first instinct was to do it entirely by hand, and that failure is the reason Dokploy made sense to me later. I rented a VPS, installed Node, set up PM2 to keep the process alive, hand-wrote an Nginx config, and ran Certbot for a certificate. I felt clever for about four days.&lt;/p&gt;

&lt;p&gt;Then deploying turned out to mean SSH-ing in to run &lt;code&gt;git pull&lt;/code&gt;, which is fine right up until you are doing it from your phone because something has broken. No build logs, no deployment history, and a rollback plan that was really just hope. Adding a second app meant another Nginx block, another port, another process to babysit.&lt;/p&gt;

&lt;p&gt;The problem was never that servers are hard. It was that I had hand-built a worse deployment platform and was now responsible for maintaining it. Once I wrote down what I actually wanted, it became shoppable: something that handled the reverse proxy, the certificates, the builds, the restarts, and the rollbacks, on hardware I paid for directly. That is what Dokploy is. So I went and got a server to put it on.&lt;/p&gt;




&lt;h2&gt;
  
  
  Choosing the Server, and the Mistake I Made in the First Hour
&lt;/h2&gt;

&lt;p&gt;I went with DigitalOcean. Hetzner, Vultr, and OVH are all fine too, and cheaper if you are optimising purely on price, but I already had projects there and wanted one dashboard rather than four.&lt;/p&gt;

&lt;p&gt;Dokploy says the minimum is 2GB of RAM. That is technically true and practically a trap, because you build your application on the same box you deploy to.&lt;/p&gt;

&lt;p&gt;I found out the hard way. My first build died on the $12 droplet. Not failed, died. The log stopped part way through with no error, and I spent twenty minutes blaming my Dockerfile before finding the kernel had killed it for using too much memory. Skip the entry tiers and start on the 4GB droplet at $24, which fits the app, the database, and the build at once. Ubuntu 24.04, because boring is correct here.&lt;/p&gt;

&lt;p&gt;One more thing before you install anything, and I nearly skipped it. Ten minutes on the basics: SSH key instead of password login, password login disabled, firewall with ports 22, 80, 443, and 3000 open. When I checked my auth log a day later there were thousands of failed login attempts. Nobody knew my server existed. That is simply what the internet does to a public IP.&lt;/p&gt;

&lt;p&gt;With the box locked down, the install itself is almost anticlimactic.&lt;/p&gt;




&lt;h2&gt;
  
  
  Installing Dokploy Takes One Command
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sSL&lt;/span&gt; https://dokploy.com/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few minutes of output, and it is done. It is worth seeing what that script did for you, because you now own a real stack rather than a magic box. It installed Docker, set up Swarm mode so containers restart on their own when they die, deployed Traefik as your reverse proxy, and started a small database for Dokploy itself. Every one of those I configured by hand the first time, and got wrong at least once.&lt;/p&gt;

&lt;p&gt;Open your server IP on port 3000 and create an admin account. Then, before deploying anything, point a subdomain like &lt;code&gt;panel.yourdomain.com&lt;/code&gt; at the server, give the dashboard that domain, turn on HTTPS, and close port 3000. Your control panel should not sit on an open IP with only a password in front of it.&lt;/p&gt;

&lt;p&gt;You now have a platform. What it does not have yet is your code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Connecting GitHub Is the Decision That Pays Off Later
&lt;/h2&gt;

&lt;p&gt;Dokploy pulls your code two ways, and the choice matters more than it looks at the time. The first is a generic Git connection over SSH, which works with GitLab, Bitbucket, Gitea, or anything else that speaks SSH. Generate a key in the SSH Keys section, add the public half to your provider, and use the &lt;code&gt;git@&lt;/code&gt; URL rather than &lt;code&gt;https&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The second, if you are on GitHub, is the Dokploy GitHub App. Two extra minutes, and it is the best decision in this story. That single step gave me automatic deploys on push, watch paths, and preview environments for pull requests, with no further configuration. Those are exactly the three things people assume they give up when they leave managed hosting, and this is where you keep them. Click the right button early, because switching later is far more annoying.&lt;/p&gt;

&lt;p&gt;Then create a project, pick your repository and branch, and paste your environment variables into a box that works like a &lt;code&gt;.env&lt;/code&gt; file. One detail that confused me: Dokploy clones the repository onto your own server and builds it there, which is why server memory mattered so much a moment ago.&lt;/p&gt;

&lt;p&gt;Then I hit deploy. And then I hit it three more times.&lt;/p&gt;




&lt;h2&gt;
  
  
  My First Three Deploys Failed, and That Was the Useful Part
&lt;/h2&gt;

&lt;p&gt;Every tutorial I read showed a green checkmark on the first attempt, so when mine went red I assumed something was structurally wrong. It was not. Three ordinary mistakes, each of which explained a piece of how the system fits together.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Port
&lt;/h3&gt;

&lt;p&gt;The build worked, the container started, and the site returned a gateway error, because I had never told Dokploy which port my app listens on. That field lives in the domain settings rather than the build settings, which is why I missed it. The lesson is worth keeping: your app is never exposed to the internet directly. Traefik forwards public traffic inward, which is why you never open your app port in the firewall.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2l0e33lsxkpeh60swimy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2l0e33lsxkpeh60swimy.png" alt="The path a request takes, and why the container port lives in the domain settings" width="799" height="316"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The path a request takes, and why the container port lives in the domain settings.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Missing Environment Variable
&lt;/h3&gt;

&lt;p&gt;The second was a missing environment variable. It built fine and crashed on startup, and the container log said so in its first line while I was staring at the build log. Those answer different questions: why it would not compile, versus why the compiled thing will not run.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. The Build Type
&lt;/h3&gt;

&lt;p&gt;The third was the build type, which is how Dokploy turns your repository into a running app at all. There are five options, and the default guesses for you.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;If your project is&lt;/th&gt;
&lt;th&gt;Choose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;A standard Next.js, Django, or Laravel app&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Railpack, the modern default, or Nixpacks if it gives you trouble&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Already carrying a Dockerfile you trust&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Dockerfile, for full control including build secrets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;A static site from Vite, Astro, or Hugo&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Nixpacks with a Publish Directory, or Static if pre-built&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Being moved directly off Heroku&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Buildpack, using your existing Heroku or Paketo config&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I switched to a Dockerfile because my build needed a step the automatic builders were skipping. Fourth deploy, green checkmark. The site loaded, at an IP address, which did not feel like it counted yet.&lt;/p&gt;


&lt;h2&gt;
  
  
  Giving It a Real Domain and Free HTTPS
&lt;/h2&gt;

&lt;p&gt;This happens in two steps, and doing them in the wrong order caused my next mistake. At your registrar, create an A record pointing your domain at the server IP. Then in Dokploy, open your app, go to Domains, and add one. Set the Host, Path, and Container Port, flip the HTTPS toggle, choose Let’s Encrypt, and the certificate and its renewal are handled from then on.&lt;/p&gt;

&lt;p&gt;My mistake was enabling HTTPS fifteen seconds after creating the DNS record. Let’s Encrypt tried to verify I owned the domain, found nothing, and failed with an error that never mentioned timing. Run &lt;code&gt;dig +short yourdomain.com&lt;/code&gt; first, and only enable HTTPS once it prints your server IP back.&lt;/p&gt;

&lt;p&gt;One more trap catches a lot of people. If your DNS is on Cloudflare, the orange cloud means Cloudflare is proxying your traffic, and verification fails with an error that never mentions Cloudflare either. Set the record to DNS only until the certificate is issued, then switch proxying back on if you want it.&lt;/p&gt;

&lt;p&gt;While you are in the dashboard, add Postgres too. Resist the urge to expose its port, because open database ports are among the most scanned things on the internet. Your app reaches it by service name over the internal Docker network, as in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;postgresql://user:pass@my-app-db:5432/mydb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that was the setup finished. Custom domain, valid certificate, deploys on push, a database, all for $24 a month. It ran perfectly for two weeks, which is when the real education started.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two Weeks In: The Monorepo Problem
&lt;/h2&gt;

&lt;p&gt;The first crack was small. My repo is a monorepo, and every push rebuilt everything in it. I would fix a typo in a markdown file and watch three services redeploy and send me notifications for things that had not changed.&lt;/p&gt;

&lt;p&gt;Watch paths fix this. In your app’s Git settings you list the files and folders that app actually cares about, and it only rebuilds when something inside them changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apps/web/**
packages/ui/**
!apps/web/**/*.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two asterisks match across directories, and a leading exclamation mark excludes. One caveat before you go debugging: this works out of the box on GitHub, but on GitLab, Bitbucket, or Gitea you need automatic deployment working first, because watch paths filter automatic deploys. With nothing to filter, they look ignored.&lt;/p&gt;

&lt;p&gt;That one was an annoyance. The next one was not.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three Weeks In: The Day All My User Uploads Disappeared
&lt;/h2&gt;

&lt;p&gt;I shipped a routine change, nothing dramatic, and somebody told me their profile picture was gone. Not just theirs. Every file any user had uploaded since launch.&lt;/p&gt;

&lt;p&gt;The cause is obvious in hindsight and catches almost everyone new to containers. I had been saving uploads to a folder inside the application, so when I deployed, Dokploy built a fresh container from my repository and replaced the old one. My repository does not contain user uploads. Containers are meant to be disposable, and that is exactly what makes deploys and rollbacks safe. My mistake was putting data that has to survive inside a thing designed not to.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz5orpw65d5n252vvinpd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz5orpw65d5n252vvinpd.png" alt="Why the uploads vanished, and where they should have been living instead" width="799" height="373"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Why the uploads vanished, and where they should have been living instead.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For user files the answer is object storage, and I picked Cloudflare R2 for one reason: it charges nothing for egress. Everyone else charges you to read your own files back out, and that cost scales with traffic rather than storage. R2 is $0.015 per GB per month with zero egress, against roughly $0.023 per GB plus $0.09 per GB egress on S3, and the free tier covers 10GB and 10 million reads a month.&lt;/p&gt;

&lt;p&gt;Best of all, R2 speaks the S3 API. Create a bucket, generate an API token with Object Read and Write, note your account ID, and point your existing S3 client at it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;S3Client&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@aws-sdk/client-s3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;S3Client&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`https://&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ACCOUNT_ID&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.r2.cloudflarestorage.com`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;accessKeyId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secretAccessKey&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setting that up turned out to be lucky, because a few days later I needed somewhere to put backups.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Night I Realised I Had No Backups at All
&lt;/h2&gt;

&lt;p&gt;I was running a migration late one night, and for about ninety seconds I believed I had destroyed my users table. I had not, in the end. But in those ninety seconds I went looking for my backups and found something worse than a bad migration. There were none. Three weeks of production data, not a single copy anywhere.&lt;/p&gt;

&lt;p&gt;The embarrassing part is that Dokploy had this built in the whole time, in a tab I had scrolled past a dozen times. On the schedule you set, it runs a proper database dump, the same &lt;code&gt;pg_dump&lt;/code&gt; you would run by hand, and ships it to S3-compatible storage. Which is why the R2 bucket from the last section quietly became my backup destination too, on the same free tier.&lt;/p&gt;

&lt;p&gt;Add the destination once under Settings, then Destinations, using your R2 access key, secret key, bucket, the region set to &lt;code&gt;auto&lt;/code&gt;, and the same endpoint as above. Hit the test button before you trust it, because a destination that fails silently is worse than none. Then open your database, go to Backups, and set a schedule. I use &lt;code&gt;0 3 * * *&lt;/code&gt; for daily at 3am.&lt;/p&gt;

&lt;p&gt;Now the part that matters more than everything above it. A backup you have never restored is not a backup, it is a file you feel good about. The week after setting this up I spun up a throwaway Postgres service and restored into it on purpose. Ten minutes, and it worked. Do that once while nothing is on fire. Then be as pleased with yourself as I was, right up until you try to back up the next thing.&lt;/p&gt;




&lt;h2&gt;
  
  
  What to Do When There Is No Database to Back Up
&lt;/h2&gt;

&lt;p&gt;My other service had no database. It kept its state in a folder, which is the shape of anything using SQLite or any self-hosted tool with a working directory, so the backup feature had nothing to point at.&lt;/p&gt;

&lt;p&gt;Dokploy handles this with volume backups, which meant finally understanding a distinction I had been ignoring. A bind mount points at a folder on the host machine. A named volume is storage Docker itself creates and manages, referred to by name. I assumed that was stylistic. It is not, because volume backups only work with named volumes, so a bind mount means migrating first.&lt;/p&gt;

&lt;p&gt;Creating one is easy. Go to Advanced, then Mounts, and add a Volume Mount with a name like &lt;code&gt;app_data&lt;/code&gt; and the path where it appears inside the container. One detail that will confuse you at restore time: for Compose services, Dokploy prefixes the volume name with the app name, so &lt;code&gt;app_data&lt;/code&gt; exists as &lt;code&gt;myapp-abc123_app_data&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You also choose whether to stop the container while the backup runs. Stopping is safer, since nothing is writing while files are copied. I stop mine, because forty seconds of downtime at 3am costs nothing and a corrupted backup would cost a great deal.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2w0tup5h2xh1nnx0kch1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2w0tup5h2xh1nnx0kch1.png" alt="The finished storage setup: uploads written live, database and volumes backed up on a schedule, all into one bucket" width="799" height="318"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The finished storage setup: uploads written live, database and volumes backed up on a schedule, all into one bucket.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;With that, my data was safe. So naturally the server got slow.&lt;/p&gt;


&lt;h2&gt;
  
  
  A Month Later: Why Is Everything Suddenly Slow?
&lt;/h2&gt;

&lt;p&gt;Everything got sluggish at once, across all my apps, which is the least helpful symptom possible because it tells you the problem is shared and nothing more.&lt;/p&gt;

&lt;p&gt;Under my old setup I would have SSH-ed in, run &lt;code&gt;top&lt;/code&gt;, squinted at container-generated process names, and guessed. Instead I opened the dashboard, where every application has its own view of CPU, memory, disk, and network usage. I found the culprit in two minutes: a background job in my API had a runaway loop and was eating almost all the memory on the box. Live logs and deployment history did the rest. A surprising number of mysteries resolve the moment you notice the slowdown began at the exact minute of a deploy.&lt;/p&gt;

&lt;p&gt;I fixed the loop, obviously. More usefully, I set a memory limit on that app under Advanced, then Resources, so it can never take the whole server down again. If it exceeds the limit it restarts rather than consuming everything around it. I now set limits on everything at double normal usage, plus alert thresholds pointed at Discord so the server tells me before a user does.&lt;/p&gt;

&lt;p&gt;Worth being straight about the limits. Self-hosted gives you per-container metrics, logs, deployment history, resource limits, and threshold alerts. The advanced dashboard is part of Dokploy’s paid cloud tier. For a handful of apps the built-in view is plenty, and if you outgrow it, Dokploy ships a Prometheus extension as a template that gives you Grafana on the same box.&lt;/p&gt;

&lt;p&gt;That is the last of my four mistakes. One more warning, though, and it is the thing that quietly kills self-hosted setups around the three month mark.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Thing That Will Break in Three Months
&lt;/h2&gt;

&lt;p&gt;Your disk will fill up. Every deploy builds a new Docker image, and old images and build caches do not delete themselves. You go from comfortable to out of space with no warning at all, and your deploys start failing with errors that never say "disk full".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt;                    &lt;span class="c"&gt;# how much space is left&lt;/span&gt;
docker system prune &lt;span class="nt"&gt;-af&lt;/span&gt;  &lt;span class="c"&gt;# remove unused images and caches&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run that occasionally, and update Dokploy itself from the dashboard every month or so, since it is your software now.&lt;/p&gt;




&lt;h2&gt;
  
  
  So, Was It Worth It?
&lt;/h2&gt;

&lt;p&gt;$198 a month became $24, and R2 is still inside the free tier. It cost me a weekend including every mistake above, and about thirty minutes a month since. Nothing about my day-to-day changed: deploy on push, automatic HTTPS, preview environments, and one-click rollbacks all survived the move. That droplet now runs four applications instead of one, and adding a fifth would not change the bill.&lt;/p&gt;

&lt;p&gt;What I gave up is real, and I would rather say it plainly than sell you something. One server is one point of failure, and if the machine dies, everything on it is down until I bring it back. My answer is backups I have actually tested, and knowing a fresh server plus a restore is a couple of hours rather than a catastrophe.&lt;/p&gt;

&lt;p&gt;So do not do this if you are pre-launch and your only real job is shipping features, because every hour on infrastructure then is an hour not spent finding out whether anyone wants the thing. Do not do this if nobody can respond when the server has a problem, because a managed platform is partly an on-call rotation you are renting. And if your traffic is global and latency-sensitive, remember that one server is in one place.&lt;/p&gt;

&lt;p&gt;What I got beyond the money mattered more than I expected. Deployment used to be a black box I posted code into and hoped about. Now I know what a reverse proxy does, why containers are disposable, and where my data actually lives. The anxiety about next month’s bill is gone too, not because $24 is nothing, but because it does not move when I have a good week.&lt;/p&gt;

&lt;p&gt;If you want to try this, rent a box, spend ten minutes on SSH keys and a firewall, run one install command, and deploy something small you would not mind breaking. Give yourself an afternoon. Worst case, you destroy a server costing a few dollars and start again, which is a cheap way to learn how your own infrastructure actually works.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sources and Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.dokploy.com" rel="noopener noreferrer"&gt;Dokploy Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.dokploy.com/docs/core/applications/build-type" rel="noopener noreferrer"&gt;Dokploy Build Types&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.dokploy.com/docs/core/watch-paths" rel="noopener noreferrer"&gt;Dokploy Watch Paths&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.dokploy.com/docs/core/databases/backups" rel="noopener noreferrer"&gt;Dokploy Backups&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.cloudflare.com/r2/pricing/" rel="noopener noreferrer"&gt;Cloudflare R2 Pricing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devops</category>
      <category>nextjs</category>
      <category>docker</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
