-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathselfhosting.html
More file actions
1234 lines (1036 loc) · 46.9 KB
/
selfhosting.html
File metadata and controls
1234 lines (1036 loc) · 46.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VPS Setup & Self-Hosting - Better Dev</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
</head>
<body>
<header class="topbar">
<button class="sidebar-toggle" aria-label="Open navigation" aria-expanded="false">
<span class="hamburger-icon"></span>
</button>
<a href="index.html" class="logo">Better Dev</a>
</header>
<div class="sidebar-backdrop" aria-hidden="true"></div>
<aside class="sidebar" aria-label="Site navigation">
<div class="sidebar-header">
<span class="sidebar-title">Navigation</span>
<button class="sidebar-close" aria-label="Close navigation">×</button>
</div>
<div class="sidebar-search">
<input type="text" class="sidebar-search-input" placeholder="Search topics..." aria-label="Search topics">
<div class="sidebar-search-results"></div>
</div>
<nav class="sidebar-nav">
<div class="sidebar-group">
<a href="index.html">Home</a>
</div>
<div class="sidebar-group">
<div class="sidebar-group-label">Mathematics</div>
<a href="pre-algebra.html">Pre-Algebra</a>
<a href="algebra.html">Algebra</a>
<a href="sequences-series.html">Sequences & Series</a>
<a href="geometry.html">Geometry</a>
<a href="calculus.html">Calculus</a>
<a href="discrete-math.html">Discrete Math</a>
<a href="linear-algebra.html">Linear Algebra</a>
<a href="probability.html">Probability & Statistics</a>
<a href="binary-systems.html">Binary & Number Systems</a>
<a href="number-theory.html">Number Theory for CP</a>
<a href="computational-geometry.html">Computational Geometry</a>
<a href="game-theory.html">Game Theory</a>
</div>
<div class="sidebar-group">
<div class="sidebar-group-label">Data Structures & Algorithms</div>
<a href="dsa-foundations.html">DSA Foundations</a>
<a href="arrays.html">Arrays & Strings</a>
<a href="stacks-queues.html">Stacks & Queues</a>
<a href="hashmaps.html">Hash Maps & Sets</a>
<a href="linked-lists.html">Linked Lists</a>
<a href="trees.html">Trees & BST</a>
<a href="graphs.html">Graphs</a>
<a href="sorting.html">Sorting & Searching</a>
<a href="patterns.html">LeetCode Patterns</a>
<a href="dp.html">Dynamic Programming</a>
<a href="advanced.html">Advanced Topics</a>
<a href="string-algorithms.html">String Algorithms</a>
<a href="advanced-graphs.html">Advanced Graphs</a>
<a href="advanced-dp.html">Advanced DP</a>
<a href="advanced-ds.html">Advanced Data Structures</a>
<a href="leetcode-650.html">The 650 Problems</a>
<a href="competitive-programming.html">CP Roadmap</a>
</div>
<div class="sidebar-group">
<div class="sidebar-group-label">Languages & Systems</div>
<a href="cpp.html">C++</a>
<a href="golang.html">Go</a>
<a href="javascript.html">JavaScript Deep Dive</a>
<a href="typescript.html">TypeScript</a>
<a href="nodejs.html">Node.js Internals</a>
<a href="os.html">Operating Systems</a>
<a href="linux.html">Linux</a>
<a href="git.html">Git</a>
<a href="backend.html">Backend</a>
<a href="system-design.html">System Design</a>
<a href="networking.html">Networking</a>
<a href="cloud.html">Cloud & Infrastructure</a>
<a href="docker.html">Docker & Compose</a>
<a href="kubernetes.html">Kubernetes</a>
<a href="message-queues.html">Queues & Pub/Sub</a>
<a href="selfhosting.html">VPS & Self-Hosting</a>
<a href="databases.html">PostgreSQL & MySQL</a>
<a href="stripe.html">Stripe & Payments</a>
<a href="distributed-systems.html">Distributed Systems</a>
<a href="backend-engineering.html">Backend Engineering</a>
</div>
<div class="sidebar-group">
<div class="sidebar-group-label">JS/TS Ecosystem</div>
<a href="js-tooling.html">Tooling & Bundlers</a>
<a href="js-testing.html">Testing</a>
<a href="ts-projects.html">Building with TS</a>
</div>
<div class="sidebar-group">
<div class="sidebar-group-label">More</div>
<a href="seans-brain.html">Sean's Brain</a>
</div>
</nav>
</aside>
<div class="container">
<!-- ===== PAGE HEADER ===== -->
<div class="page-header">
<div class="breadcrumb"><a href="index.html">Home</a> / VPS Setup & Self-Hosting</div>
<h1>VPS Setup & Self-Hosting</h1>
<p>Learn how to set up a VPS from scratch, secure it, deploy your apps, and self-host services using tools like Coolify, Caddy, and Docker. Stop paying $20/month for Vercel Pro when a $5 VPS can do it all.</p>
</div>
<!-- ===== TABLE OF CONTENTS ===== -->
<div class="toc">
<h4>Table of Contents</h4>
<a href="#why-selfhost">1. Why Self-Host?</a>
<a href="#choosing-vps">2. Choosing a VPS Provider</a>
<a href="#initial-setup">3. Initial Server Setup</a>
<a href="#ssh">4. SSH Keys & Secure Access</a>
<a href="#firewall">5. Firewall & Security Hardening</a>
<a href="#domains-dns">6. Domains & DNS</a>
<a href="#reverse-proxy">7. Reverse Proxy (Caddy & Nginx)</a>
<a href="#ssl">8. SSL/TLS with Let's Encrypt</a>
<a href="#docker-on-vps">9. Docker on Your VPS</a>
<a href="#coolify">10. Coolify -- Your Own PaaS</a>
<a href="#other-tools">11. Other Self-Hosting Tools</a>
<a href="#monitoring">12. Monitoring & Logs</a>
<a href="#backups">13. Backups</a>
<a href="#example-deploy">14. Full Example: Deploy a Next.js App</a>
<a href="#cheatsheet">15. Command Cheat Sheet</a>
</div>
<!-- ============================================================ -->
<!-- SECTION 1: WHY SELF-HOST -->
<!-- ============================================================ -->
<section id="why-selfhost">
<h2>1. Why Self-Host?</h2>
<p>Every time you deploy to Vercel, Netlify, or Railway, you're using <strong>someone else's server</strong>. Self-hosting means running your own server and controlling the entire stack. Here's why that matters:</p>
<table>
<tr>
<th>Managed Platforms</th>
<th>Self-Hosting</th>
</tr>
<tr>
<td>Easy to start, expensive to scale</td>
<td>Small learning curve, cheap forever</td>
</tr>
<tr>
<td>Vendor lock-in (proprietary configs)</td>
<td>Standard Linux -- works anywhere</td>
</tr>
<tr>
<td>Limited control over infra</td>
<td>Full root access, install anything</td>
</tr>
<tr>
<td>Bandwidth/build limits on free tiers</td>
<td>No artificial limits</td>
</tr>
<tr>
<td>$20+/month for Pro features</td>
<td>$4-6/month for a full VPS</td>
</tr>
</table>
<div class="tip-box">
<div class="label">When to Self-Host vs Use Managed</div>
<p><strong>Self-host</strong> when you want to learn infrastructure, save money at scale, host multiple projects on one server, or need full control. <strong>Use managed platforms</strong> for quick prototypes, when you have zero time for ops, or when your team has no Linux experience and can't afford downtime.</p>
</div>
<p>Self-hosting teaches you Linux, networking, Docker, DNS, security, and deployment -- skills that make you a significantly more capable developer and a stronger hire.</p>
</section>
<!-- ============================================================ -->
<!-- SECTION 2: CHOOSING A VPS -->
<!-- ============================================================ -->
<section id="choosing-vps">
<h2>2. Choosing a VPS Provider</h2>
<p>A <strong>VPS</strong> (Virtual Private Server) is a virtual machine running on shared hardware. You get root access to your own isolated Linux environment. Here are the best options:</p>
<table>
<tr>
<th>Provider</th>
<th>Cheapest Plan</th>
<th>Strengths</th>
<th>Best For</th>
</tr>
<tr>
<td><strong>Hetzner</strong></td>
<td>~$4/mo (2 vCPU, 4GB RAM)</td>
<td>Best price-to-performance ratio, EU data centers, great ARM options</td>
<td>Most self-hosters, best value</td>
</tr>
<tr>
<td><strong>DigitalOcean</strong></td>
<td>$6/mo (1 vCPU, 1GB RAM)</td>
<td>Excellent docs, simple UI, large community</td>
<td>Beginners, good tutorials</td>
</tr>
<tr>
<td><strong>Linode (Akamai)</strong></td>
<td>$5/mo (1 vCPU, 1GB RAM)</td>
<td>Reliable, good support, competitive pricing</td>
<td>General purpose</td>
</tr>
<tr>
<td><strong>Vultr</strong></td>
<td>$6/mo (1 vCPU, 1GB RAM)</td>
<td>Many locations, bare metal options</td>
<td>Global coverage</td>
</tr>
<tr>
<td><strong>Oracle Cloud</strong></td>
<td>Free tier (4 ARM cores, 24GB RAM)</td>
<td>Insanely generous free tier</td>
<td>Learning/experimenting (unreliable availability)</td>
</tr>
</table>
<div class="tip-box">
<div class="label">Recommendation</div>
<p><strong>Start with Hetzner.</strong> Their CX22 (2 vCPU, 4GB RAM, 40GB SSD) at ~$4/month is unbeatable. Choose Ubuntu 24.04 LTS as your OS. Pick the closest data center to your users (Falkenstein or Helsinki for EU, Ashburn for US East).</p>
</div>
</section>
<!-- ============================================================ -->
<!-- SECTION 3: INITIAL SERVER SETUP -->
<!-- ============================================================ -->
<section id="initial-setup">
<h2>3. Initial Server Setup</h2>
<p>You just created a VPS. Here's the step-by-step to go from a fresh Ubuntu install to a secure, ready-to-deploy server.</p>
<h3>Step 1: Log In as Root</h3>
<p>Your provider gives you a root password or lets you add an SSH key during creation. Connect for the first time:</p>
<pre><code># Replace with your server's IP address
ssh root@YOUR_SERVER_IP</code></pre>
<h3>Step 2: Update the System</h3>
<pre><code>apt update && apt upgrade -y</code></pre>
<h3>Step 3: Create a Non-Root User</h3>
<p>Never run everything as root. Create a regular user with sudo privileges:</p>
<pre><code># Create user
adduser deploy
# Add to sudo group
usermod -aG sudo deploy</code></pre>
<h3>Step 4: Set the Hostname</h3>
<pre><code>hostnamectl set-hostname myserver</code></pre>
<h3>Step 5: Set the Timezone</h3>
<pre><code>timedatectl set-timezone UTC</code></pre>
<div class="tip-box">
<div class="label">Why UTC?</div>
<p>Always set servers to UTC. Your logs, cron jobs, and timestamps stay consistent regardless of where you or your users are. Convert to local time in your application layer.</p>
</div>
<h3>Step 6: Install Essential Packages</h3>
<pre><code>apt install -y curl wget git unzip htop ufw fail2ban</code></pre>
</section>
<!-- ============================================================ -->
<!-- SECTION 4: SSH -->
<!-- ============================================================ -->
<section id="ssh">
<h2>4. SSH Keys & Secure Access</h2>
<p>Passwords are weak. SSH keys are cryptographic key pairs -- a <strong>private key</strong> (stays on your machine) and a <strong>public key</strong> (goes on the server). The server can verify you hold the private key without it ever crossing the network.</p>
<h3>Generate an SSH Key (On Your Local Machine)</h3>
<pre><code># Generate Ed25519 key (recommended over RSA)
ssh-keygen -t ed25519 -C "your@email.com"
# This creates:
# ~/.ssh/id_ed25519 (private key -- NEVER share this)
# ~/.ssh/id_ed25519.pub (public key -- goes on server)</code></pre>
<h3>Copy the Public Key to Your Server</h3>
<pre><code># Method 1: ssh-copy-id (easiest)
ssh-copy-id deploy@YOUR_SERVER_IP
# Method 2: Manual
cat ~/.ssh/id_ed25519.pub | ssh root@YOUR_SERVER_IP "mkdir -p /home/deploy/.ssh && cat >> /home/deploy/.ssh/authorized_keys"</code></pre>
<h3>Lock Down SSH (On the Server)</h3>
<p>Edit the SSH config to disable password login and root login:</p>
<pre><code>sudo nano /etc/ssh/sshd_config</code></pre>
<p>Find and change these lines:</p>
<pre><code># Disable password authentication
PasswordAuthentication no
# Disable root login
PermitRootLogin no
# Optional: change default port (makes automated scanners miss you)
# Port 2222</code></pre>
<p>Restart SSH:</p>
<pre><code>sudo systemctl restart sshd</code></pre>
<div class="warning-box">
<div class="label">Don't Lock Yourself Out</div>
<p>Before disabling password auth, open a <strong>second terminal</strong> and verify you can log in with your SSH key as the <code>deploy</code> user. If you disable passwords and your key doesn't work, you'll be locked out permanently.</p>
</div>
<h3>SSH Config File (On Your Local Machine)</h3>
<p>Save connection details so you can type <code>ssh myserver</code> instead of the full command:</p>
<pre><code># ~/.ssh/config
Host myserver
HostName YOUR_SERVER_IP
User deploy
IdentityFile ~/.ssh/id_ed25519
# Port 2222 (if you changed the port)</code></pre>
<p>Now just run:</p>
<pre><code>ssh myserver</code></pre>
</section>
<!-- ============================================================ -->
<!-- SECTION 5: FIREWALL & SECURITY -->
<!-- ============================================================ -->
<section id="firewall">
<h2>5. Firewall & Security Hardening</h2>
<h3>UFW (Uncomplicated Firewall)</h3>
<p>UFW is a frontend for iptables that makes firewall rules human-readable:</p>
<pre><code># Allow SSH (do this FIRST or you'll lock yourself out)
sudo ufw allow OpenSSH
# Allow HTTP and HTTPS
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Enable the firewall
sudo ufw enable
# Check status
sudo ufw status verbose</code></pre>
<div class="example-box">
<div class="label">UFW Status Output</div>
<pre><code>Status: active
To Action From
-- ------ ----
OpenSSH ALLOW Anywhere
80/tcp ALLOW Anywhere
443/tcp ALLOW Anywhere</code></pre>
</div>
<h3>Fail2Ban</h3>
<p>Fail2Ban watches log files and bans IPs that show malicious signs (like brute-force SSH attempts):</p>
<pre><code># It's already installed from our earlier step
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
# Check banned IPs
sudo fail2ban-client status sshd</code></pre>
<h3>Automatic Security Updates</h3>
<pre><code>sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades</code></pre>
<div class="tip-box">
<div class="label">Security Checklist</div>
<p>1. SSH key auth only (no passwords). 2. Non-root user with sudo. 3. UFW firewall enabled. 4. Fail2Ban running. 5. Auto security updates. 6. Keep software updated. This covers 95% of threats for a personal VPS.</p>
</div>
</section>
<!-- ============================================================ -->
<!-- SECTION 6: DOMAINS & DNS -->
<!-- ============================================================ -->
<section id="domains-dns">
<h2>6. Domains & DNS</h2>
<p>DNS (Domain Name System) translates human-readable domain names to IP addresses. To point your domain at your VPS, you need to create DNS records.</p>
<h3>Where to Buy Domains</h3>
<table>
<tr>
<th>Registrar</th>
<th>Why</th>
</tr>
<tr>
<td><strong>Cloudflare Registrar</strong></td>
<td>At-cost pricing (no markup), free DNS, free proxy/CDN</td>
</tr>
<tr>
<td><strong>Namecheap</strong></td>
<td>Good prices, easy UI, free WhoisGuard</td>
</tr>
<tr>
<td><strong>Porkbun</strong></td>
<td>Cheapest for many TLDs, clean interface</td>
</tr>
</table>
<h3>Essential DNS Records</h3>
<table>
<tr>
<th>Record Type</th>
<th>Name</th>
<th>Value</th>
<th>What It Does</th>
</tr>
<tr>
<td><strong>A</strong></td>
<td>@</td>
<td>YOUR_SERVER_IP</td>
<td>Points <code>yourdomain.com</code> to your server</td>
</tr>
<tr>
<td><strong>A</strong></td>
<td>*</td>
<td>YOUR_SERVER_IP</td>
<td>Wildcard -- points <code>*.yourdomain.com</code> to your server</td>
</tr>
<tr>
<td><strong>CNAME</strong></td>
<td>www</td>
<td>yourdomain.com</td>
<td>Points <code>www.yourdomain.com</code> to root domain</td>
</tr>
</table>
<div class="example-box">
<div class="label">Example: Cloudflare DNS Setup</div>
<pre><code>Type Name Content Proxy TTL
A @ 203.0.113.42 DNS only Auto
A * 203.0.113.42 DNS only Auto
CNAME www yourdomain.com DNS only Auto</code></pre>
<p>Set proxy to "DNS only" (grey cloud) if you're handling SSL yourself with Caddy or Let's Encrypt. Use "Proxied" (orange cloud) if you want Cloudflare's CDN and DDoS protection.</p>
</div>
<h3>Verify DNS Propagation</h3>
<pre><code># Check if your domain resolves to your IP
dig yourdomain.com +short
# Or use nslookup
nslookup yourdomain.com</code></pre>
<div class="tip-box">
<div class="label">DNS Propagation</div>
<p>DNS changes can take 5 minutes to 48 hours to propagate worldwide, but usually it's under 30 minutes. If you're using Cloudflare DNS, changes are nearly instant.</p>
</div>
</section>
<!-- ============================================================ -->
<!-- SECTION 7: REVERSE PROXY -->
<!-- ============================================================ -->
<section id="reverse-proxy">
<h2>7. Reverse Proxy (Caddy & Nginx)</h2>
<p>A <strong>reverse proxy</strong> sits in front of your applications and routes incoming requests to the right service. It handles SSL, load balancing, and lets you run multiple apps on one server.</p>
<div class="example-box">
<div class="label">How It Works</div>
<pre><code>Internet
│
▼
┌──────────────────┐
│ Reverse Proxy │ ← Listens on ports 80 & 443
│ (Caddy/Nginx) │
└──────┬───────────┘
│
┌───┼────────────────┐
▼ ▼ ▼
:3000 :8080 :5000
App 1 App 2 App 3
(Next) (API) (Flask)
app1.yourdomain.com → :3000
api.yourdomain.com → :8080
app3.yourdomain.com → :5000</code></pre>
</div>
<h3>Option A: Caddy (Recommended)</h3>
<p>Caddy is a modern web server that <strong>automatically handles SSL</strong> via Let's Encrypt. Zero config for HTTPS. This is the easiest option.</p>
<h4>Install Caddy</h4>
<pre><code>sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy</code></pre>
<h4>Caddyfile Configuration</h4>
<pre><code># /etc/caddy/Caddyfile
# App 1 -- Next.js running on port 3000
app1.yourdomain.com {
reverse_proxy localhost:3000
}
# App 2 -- API running on port 8080
api.yourdomain.com {
reverse_proxy localhost:8080
}
# Static site
yourdomain.com {
root * /var/www/mysite
file_server
}</code></pre>
<p>That's it. Caddy automatically provisions SSL certificates. Reload with:</p>
<pre><code>sudo systemctl reload caddy</code></pre>
<h3>Option B: Nginx</h3>
<p>Nginx is the industry standard. More config required, but maximum flexibility.</p>
<h4>Install Nginx</h4>
<pre><code>sudo apt install -y nginx</code></pre>
<h4>Nginx Site Configuration</h4>
<pre><code># /etc/nginx/sites-available/app1
server {
listen 80;
server_name app1.yourdomain.com;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}</code></pre>
<pre><code># Enable the site
sudo ln -s /etc/nginx/sites-available/app1 /etc/nginx/sites-enabled/
sudo nginx -t # Test config
sudo systemctl reload nginx</code></pre>
<div class="tip-box">
<div class="label">Caddy vs Nginx</div>
<p><strong>Use Caddy</strong> for personal projects and when you want zero SSL hassle. <strong>Use Nginx</strong> when you need advanced features (rate limiting, caching, complex rewrites) or when you're in a team that already uses it. Both are production-ready.</p>
</div>
</section>
<!-- ============================================================ -->
<!-- SECTION 8: SSL/TLS -->
<!-- ============================================================ -->
<section id="ssl">
<h2>8. SSL/TLS with Let's Encrypt</h2>
<p>SSL/TLS encrypts traffic between your users and your server. <strong>Let's Encrypt</strong> provides free, auto-renewing SSL certificates.</p>
<div class="tip-box">
<div class="label">If You're Using Caddy</div>
<p>Skip this section. Caddy handles SSL automatically -- it provisions and renews Let's Encrypt certificates for every domain in your Caddyfile with zero configuration.</p>
</div>
<h3>Certbot (For Nginx)</h3>
<pre><code># Install Certbot
sudo apt install -y certbot python3-certbot-nginx
# Get a certificate (Certbot auto-configures Nginx)
sudo certbot --nginx -d app1.yourdomain.com -d api.yourdomain.com
# Test auto-renewal
sudo certbot renew --dry-run</code></pre>
<p>Certbot adds a cron job that automatically renews certificates before they expire (every 90 days).</p>
<h3>Verify SSL</h3>
<pre><code># Check certificate details
curl -vI https://app1.yourdomain.com 2>&1 | grep -A5 "Server certificate"
# Or use openssl
openssl s_client -connect app1.yourdomain.com:443 -servername app1.yourdomain.com</code></pre>
</section>
<!-- ============================================================ -->
<!-- SECTION 9: DOCKER ON YOUR VPS -->
<!-- ============================================================ -->
<section id="docker-on-vps">
<h2>9. Docker on Your VPS</h2>
<p>Docker is the standard way to deploy applications on a VPS. Instead of installing dependencies directly on the server, you package everything into containers. Check the <a href="docker.html">Docker page</a> for fundamentals -- this section covers VPS-specific setup.</p>
<h3>Install Docker</h3>
<pre><code># Install Docker using the official convenience script
curl -fsSL https://get.docker.com | sh
# Add your user to the docker group (no sudo needed for docker commands)
sudo usermod -aG docker deploy
# Log out and back in for group changes to take effect
exit
ssh myserver
# Verify
docker --version
docker compose version</code></pre>
<h3>Docker Compose on a VPS</h3>
<p>A typical production setup with Docker Compose:</p>
<pre><code># docker-compose.yml
services:
app:
build: .
restart: unless-stopped
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://user:pass@db:5432/myapp
depends_on:
- db
db:
image: postgres:16-alpine
restart: unless-stopped
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=myapp
redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:</code></pre>
<pre><code># Deploy
docker compose up -d
# View logs
docker compose logs -f app
# Update and redeploy
git pull
docker compose up -d --build</code></pre>
<div class="warning-box">
<div class="label">Environment Variables in Production</div>
<p>Never hardcode secrets in <code>docker-compose.yml</code>. Use a <code>.env</code> file (and add it to <code>.gitignore</code>) or Docker secrets. The example above uses inline values for clarity only.</p>
</div>
</section>
<!-- ============================================================ -->
<!-- SECTION 10: COOLIFY -->
<!-- ============================================================ -->
<section id="coolify">
<h2>10. Coolify -- Your Own PaaS</h2>
<p><strong>Coolify</strong> is an open-source, self-hosted alternative to Vercel, Netlify, and Heroku. It gives you a web UI to deploy applications, databases, and services with automatic SSL, GitHub integration, and zero-downtime deployments -- all running on your own VPS.</p>
<div class="example-box">
<div class="label">What Coolify Replaces</div>
<pre><code>Vercel / Netlify → Frontend deployments with preview URLs
Heroku / Railway → Backend app hosting with env vars
PlanetScale / Neon → Managed databases (Postgres, MySQL, Redis)
GitHub Actions → Build & deploy pipelines
All of this in ONE tool, on YOUR server, for $0/month in software costs.</code></pre>
</div>
<h3>Install Coolify</h3>
<p>Coolify has a one-line installer. Run this on a <strong>fresh VPS</strong> (or one where ports 80/443 aren't already in use):</p>
<pre><code>curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash</code></pre>
<p>After installation, access the Coolify dashboard at <code>http://YOUR_SERVER_IP:8000</code>. Set up your admin account on first visit.</p>
<h3>What Coolify Can Deploy</h3>
<table>
<tr>
<th>Type</th>
<th>Examples</th>
</tr>
<tr>
<td><strong>Apps from Git</strong></td>
<td>Next.js, Nuxt, SvelteKit, Express, Django, Rails, Go, Rust -- anything with a Dockerfile or Nixpacks</td>
</tr>
<tr>
<td><strong>Docker Compose</strong></td>
<td>Paste your docker-compose.yml and Coolify manages it</td>
</tr>
<tr>
<td><strong>Databases</strong></td>
<td>PostgreSQL, MySQL, MariaDB, MongoDB, Redis, Dragonfly</td>
</tr>
<tr>
<td><strong>One-Click Services</strong></td>
<td>Plausible Analytics, Umami, Ghost, WordPress, Gitea, Minio, n8n, Uptime Kuma</td>
</tr>
</table>
<h3>Deploy a Next.js App with Coolify</h3>
<div class="roadmap">
<div class="roadmap-step">
<div class="step-num">1</div>
<div class="step-label">Connect GitHub</div>
<div class="step-desc">Settings → Sources → Add GitHub App. Authorize Coolify to access your repos.</div>
</div>
<div class="roadmap-step">
<div class="step-num">2</div>
<div class="step-label">Add New Resource</div>
<div class="step-desc">Projects → Add Resource → Public/Private Repository. Select your Next.js repo.</div>
</div>
<div class="roadmap-step">
<div class="step-num">3</div>
<div class="step-label">Configure</div>
<div class="step-desc">Set build pack to Nixpacks (auto-detects Next.js), add environment variables, set the domain.</div>
</div>
<div class="roadmap-step">
<div class="step-num">4</div>
<div class="step-label">Deploy</div>
<div class="step-desc">Click Deploy. Coolify builds your app, provisions SSL, and starts the container. Future pushes auto-deploy.</div>
</div>
</div>
<h3>Coolify Features</h3>
<ul>
<li><strong>Automatic SSL</strong> -- Let's Encrypt certificates provisioned and renewed automatically</li>
<li><strong>GitHub/GitLab webhooks</strong> -- Push to deploy, just like Vercel</li>
<li><strong>Preview deployments</strong> -- Auto-deploy pull requests to preview URLs</li>
<li><strong>Databases as a service</strong> -- Spin up Postgres, Redis, etc. with one click</li>
<li><strong>Environment variables</strong> -- Manage secrets through the UI</li>
<li><strong>Logs and monitoring</strong> -- View container logs from the dashboard</li>
<li><strong>Multi-server</strong> -- Manage multiple VPS from one Coolify instance</li>
<li><strong>Backups</strong> -- Scheduled database backups to S3-compatible storage</li>
</ul>
<div class="tip-box">
<div class="label">Coolify System Requirements</div>
<p>Minimum: 2 CPU cores, 2GB RAM. Recommended: 2+ CPU cores, 4GB+ RAM. Coolify itself uses Docker, Traefik (as its built-in reverse proxy), and a SQLite/PostgreSQL database. A Hetzner CX22 ($4/mo) is perfect for this.</p>
</div>
</section>
<!-- ============================================================ -->
<!-- SECTION 11: OTHER TOOLS -->
<!-- ============================================================ -->
<section id="other-tools">
<h2>11. Other Self-Hosting Tools</h2>
<h3>Deployment & Management</h3>
<table>
<tr>
<th>Tool</th>
<th>What It Does</th>
<th>When to Use</th>
</tr>
<tr>
<td><strong>Coolify</strong></td>
<td>Full PaaS -- deploy apps, databases, services from Git</td>
<td>You want a Vercel-like experience on your own server</td>
</tr>
<tr>
<td><strong>Dokku</strong></td>
<td>Mini Heroku -- Git push to deploy with buildpacks</td>
<td>You love Heroku's <code>git push</code> workflow</td>
</tr>
<tr>
<td><strong>CapRover</strong></td>
<td>PaaS with web UI, one-click apps, and cluster support</td>
<td>Alternative to Coolify with Docker Swarm support</td>
</tr>
<tr>
<td><strong>Portainer</strong></td>
<td>Docker management UI -- see containers, logs, networks</td>
<td>You want a visual way to manage Docker</td>
</tr>
</table>
<h3>Monitoring & Analytics</h3>
<table>
<tr>
<th>Tool</th>
<th>What It Does</th>
</tr>
<tr>
<td><strong>Uptime Kuma</strong></td>
<td>Self-hosted uptime monitoring with notifications (Slack, Discord, email)</td>
</tr>
<tr>
<td><strong>Plausible Analytics</strong></td>
<td>Privacy-friendly Google Analytics alternative</td>
</tr>
<tr>
<td><strong>Umami</strong></td>
<td>Simple, fast, privacy-focused web analytics</td>
</tr>
<tr>
<td><strong>Grafana + Prometheus</strong></td>
<td>Full metrics collection and dashboard visualization</td>
</tr>
</table>
<h3>Useful Self-Hosted Services</h3>
<table>
<tr>
<th>Tool</th>
<th>What It Does</th>
</tr>
<tr>
<td><strong>Gitea / Forgejo</strong></td>
<td>Self-hosted Git (lightweight GitHub alternative)</td>
</tr>
<tr>
<td><strong>Minio</strong></td>
<td>S3-compatible object storage</td>
</tr>
<tr>
<td><strong>Vaultwarden</strong></td>
<td>Self-hosted Bitwarden password manager</td>
</tr>
<tr>
<td><strong>Traefik</strong></td>
<td>Cloud-native reverse proxy with auto-discovery for Docker containers</td>
</tr>
<tr>
<td><strong>n8n</strong></td>
<td>Self-hosted workflow automation (Zapier alternative)</td>
</tr>
<tr>
<td><strong>Ghost</strong></td>
<td>Self-hosted blog/newsletter platform</td>
</tr>
</table>
<div class="tip-box">
<div class="label">One Server, Many Services</div>
<p>With Docker and a reverse proxy, you can run 10+ services on a single $4/month VPS. Each gets its own subdomain (e.g., <code>git.yourdomain.com</code>, <code>analytics.yourdomain.com</code>, <code>status.yourdomain.com</code>) and automatic SSL.</p>
</div>
</section>
<!-- ============================================================ -->
<!-- SECTION 12: MONITORING -->
<!-- ============================================================ -->
<section id="monitoring">
<h2>12. Monitoring & Logs</h2>
<h3>Basic Server Monitoring</h3>
<pre><code># Check disk usage
df -h
# Check memory usage
free -h
# Check CPU and processes
htop
# Check who's logged in
who
# Check system uptime
uptime</code></pre>
<h3>Docker Logs</h3>
<pre><code># View logs for a specific container
docker logs -f --tail 100 container_name
# View logs for all services in a compose stack
docker compose logs -f
# View logs with timestamps
docker compose logs -f -t</code></pre>
<h3>Uptime Kuma (Recommended)</h3>
<p>Deploy with a single Docker command:</p>
<pre><code>docker run -d \
--name uptime-kuma \
--restart unless-stopped \
-p 3001:3001 \
-v uptime-kuma:/app/data \
louislam/uptime-kuma:1</code></pre>
<p>Then point <code>status.yourdomain.com</code> at port 3001 via your reverse proxy. You get a status page and notifications for when your apps go down.</p>
</section>
<!-- ============================================================ -->
<!-- SECTION 13: BACKUPS -->
<!-- ============================================================ -->
<section id="backups">
<h2>13. Backups</h2>
<p>If it's not backed up, it doesn't exist. Here's a practical backup strategy for a VPS.</p>
<div class="formula-box">
<strong>3-2-1 Backup Rule:</strong><br><br>
• <strong>3</strong> copies of your data<br>
• <strong>2</strong> different storage media (e.g., SSD + external HDD, or local + cloud)<br>
• <strong>1</strong> offsite copy (not in the same physical location)<br><br>
<strong>Test your backups.</strong> A backup you've never restored is not a backup — it's a hope.
</div>
<h3>What to Back Up</h3>
<ul>
<li><strong>Databases</strong> -- The most critical. Data you can't regenerate.</li>
<li><strong>Docker volumes</strong> -- Persistent data for your containers.</li>
<li><strong>Config files</strong> -- Caddyfile, nginx configs, docker-compose files, .env files.</li>
<li><strong>Application code</strong> -- Already in Git, so this is covered.</li>
</ul>
<h3>Database Backups</h3>
<pre><code># PostgreSQL dump
docker exec my-postgres pg_dumpall -U postgres > backup_$(date +%Y%m%d).sql
# MySQL dump
docker exec my-mysql mysqldump -u root -p --all-databases > backup_$(date +%Y%m%d).sql</code></pre>
<h3>Automated Backup Script</h3>
<pre><code>#!/bin/bash
# /home/deploy/backup.sh
BACKUP_DIR="/home/deploy/backups"
DATE=$(date +%Y%m%d_%H%M%S)
mkdir -p "$BACKUP_DIR"
# Dump PostgreSQL
docker exec postgres pg_dumpall -U postgres > "$BACKUP_DIR/db_$DATE.sql"
# Compress
gzip "$BACKUP_DIR/db_$DATE.sql"
# Delete backups older than 7 days
find "$BACKUP_DIR" -name "*.gz" -mtime +7 -delete
echo "Backup complete: db_$DATE.sql.gz"</code></pre>
<pre><code># Make it executable
chmod +x /home/deploy/backup.sh
# Add to crontab (runs daily at 3 AM)
crontab -e
# Add this line:
0 3 * * * /home/deploy/backup.sh >> /home/deploy/backups/backup.log 2>&1</code></pre>
<h3>Off-Site Backups</h3>
<p>Local backups aren't enough -- if the VPS dies, so do your backups. Send them off-site:</p>
<pre><code># Sync to another server
rsync -avz /home/deploy/backups/ backup-user@other-server:/backups/
# Or upload to S3-compatible storage (Backblaze B2, Cloudflare R2, Minio)
# Using rclone:
rclone sync /home/deploy/backups/ remote:my-backups/</code></pre>
<div class="tip-box">
<div class="label">Coolify Backups</div>
<p>If you're using Coolify, it has built-in scheduled database backups to S3-compatible storage. Configure it in the Coolify dashboard under each database's settings -- no scripts needed.</p>
</div>
</section>
<!-- ============================================================ -->
<!-- SECTION 14: FULL EXAMPLE -->
<!-- ============================================================ -->
<section id="example-deploy">
<h2>14. Full Example: Deploy a Next.js App</h2>
<p>Let's put it all together. You have a Next.js app on GitHub and a fresh VPS. Here's the manual approach (without Coolify).</p>
<h3>Step-by-Step</h3>
<pre><code># 1. SSH into your server