Initial import of Hugo site to Forgejo
This commit is contained in:
commit
cb1ba0317f
1259 changed files with 236349 additions and 0 deletions
21
public-clearnet/categories/guides/index.html
Normal file
21
public-clearnet/categories/guides/index.html
Normal file
File diff suppressed because one or more lines are too long
473
public-clearnet/categories/guides/index.xml
Normal file
473
public-clearnet/categories/guides/index.xml
Normal file
|
|
@ -0,0 +1,473 @@
|
|||
<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Guides on AlipourIm journeys</title><link>https://blog.alipour.eu/categories/guides/</link><description>Recent content in Guides on AlipourIm journeys</description><generator>Hugo -- 0.146.0</generator><language>en</language><lastBuildDate>Fri, 24 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.alipour.eu/categories/guides/index.xml" rel="self" type="application/rss+xml"/><item><title>Self-hosting mail the hard way (Postfix + Dovecot + PostfixAdmin + Roundcube, and zero Mailcow)</title><link>https://blog.alipour.eu/posts/self_hosting_mail/</link><pubDate>Fri, 24 Jul 2026 00:00:00 +0000</pubDate><guid>https://blog.alipour.eu/posts/self_hosting_mail/</guid><description>A friendly, story-shaped walk through hand-rolling mail on funbox: how email actually moves, what PTR/SPF/DKIM/DMARC even mean, why open relays are bad, and how Matrix stole my TLS certificate (again).</description><content:encoded><![CDATA[<blockquote>
|
||||
<p>If you came here scared of mail servers: good. That means you’re paying attention. This post is for you. I assume you can SSH into a Linux box and copy-paste carefully. I do <strong>not</strong> assume you already know what an MX record is, why port 587 exists, or why Cloudflare’s orange cloud and SMTP should never meet at a party.</p></blockquote>
|
||||
<p>I wanted real email on my own machine — <code>mail.alipourimjourneys.ir</code> — without buying a “smart” appliance and without installing one of those giant all-in-one stacks. Mailcow is excellent. Stalwart is excellent. I still said no. Sometimes the point is to feel each moving part with your own hands, even if that means spending an evening arguing with nginx about whether a certificate belongs to mail or to Matrix. (Spoiler: when in doubt on this box, it was Matrix.)</p>
|
||||
<p>By the end I had Postfix talking to the world, Dovecot holding the inboxes, OpenDKIM signing what I send, PostfixAdmin for creating people without SSH, Roundcube for reading mail in a browser, SPF and DKIM and DMARC in DNS, and reverse DNS (PTR) pointing the right way. Strangers cannot use my server as a free spam cannon. Thunderbird can delete messages again. I can open webmail when I’m too lazy for a desktop client. And I finally understand <em>why</em> those pieces exist, which is the part the shiny installers quietly skip.</p>
|
||||
<hr>
|
||||
<h2 id="why-i-bothered">Why I bothered</h2>
|
||||
<p>Funbox already hosted a small zoo of services with nice names and Let’s Encrypt certificates. Chat. Notes. Cloud stuff. Mail somehow stayed on the eternal TODO list labeled “next semester.” That felt silly. Email is older than most of the internet, slightly haunted, and still the boring glue that password resets and “your package has shipped” messages refuse to abandon.</p>
|
||||
<p>I also wanted something more specific than “a mailbox.” I wanted addresses under my own domain, a way to create accounts for other humans without giving them shell access, and enough understanding that when something broke at 1 a.m. I wouldn’t be guessing which logo to Google. Doing it by hand is slower. It’s also how you stop treating mail as magic.</p>
|
||||
<hr>
|
||||
<h2 id="how-email-actually-moves">How email actually moves</h2>
|
||||
<p>Imagine a post office that both receives letters from other post offices <em>and</em> lets you drop off letters to send — but only if you show ID at the counter.</p>
|
||||
<p><strong>Postfix</strong> is that post office building. It speaks SMTP. When another provider wants to deliver mail to you, it looks up your domain’s mail instructions in DNS, finds your server, and knocks on port <strong>25</strong>. That conversation is server-to-server. Gmail does not “log in as Gmail” with a password when it delivers to you. Trust comes from DNS, reputation, content filters, and whether you look like an open relay (more on that later).</p>
|
||||
<p>When <em>you</em> want to send mail from Thunderbird or Roundcube, you don’t casually open port 25 from a laptop anymore. You use <strong>submission</strong>, usually port <strong>587</strong>, after starting TLS and proving who you are with a username and password. Postfix asks Dovecot, “is this password real?” If yes, Postfix is willing to carry your message out to the wider world. If no, goodbye.</p>
|
||||
<p><strong>Dovecot</strong> is the filing cabinet and the locked reading room. It stores messages (I used Maildir: folders of files, boring and sturdy). It speaks IMAP on port <strong>993</strong> so clients can list, read, move, and delete. In the virtual setup it also accepts deliveries from Postfix over a local pipe called LMTP — think “Postfix hands the sealed envelope to the filing clerk” instead of Postfix rummaging in the drawers itself.</p>
|
||||
<p><strong>OpenDKIM</strong> sits beside Postfix like a notary. Before a message leaves, it stamps a cryptographic signature on the headers. Receiving servers can fetch your public key from DNS and check that the stamp is real. That doesn’t encrypt the love letter for privacy; it helps prove the letter wasn’t casually forged by a random stranger using your From address.</p>
|
||||
<p>Then there’s the paperwork in DNS — SPF, the DKIM public key, DMARC — which isn’t software running on your machine. It’s instructions you publish so other people’s servers know how to judge mail that claims to be from you.</p>
|
||||
<p>Once you see that split — public receiving dock on 25, gated human counter on 587, filing cabinet on 993, notary for outbound, DNS as the rulebook — the rest of the tutorial stops feeling like a random pile of config files.</p>
|
||||
<hr>
|
||||
<h2 id="the-shape-of-my-setup">The shape of my setup</h2>
|
||||
<p>On Ubuntu 24.04 on funbox, Postfix is the MTA, Dovecot is IMAP plus authentication helper plus delivery clerk, OpenDKIM signs outbound mail, PostfixAdmin is the web UI for mailboxes and aliases, and Roundcube is webmail. A small Docker Compose stack under <code>/srv/services/mail/</code> runs Postgres only for that mail world, listening on <code>127.0.0.1:5433</code>. Matrix and HedgeDoc keep their own databases. I like it that way. One sad PHP bug should not inherit a free ticket into Synapse.</p>
|
||||
<p>Nginx terminates HTTPS for a little portal at <code>mail.alipourimjourneys.ir</code>, the admin UI at <code>mailadmin.alipourimjourneys.ir</code>, and Roundcube at <code>webmail.alipourimjourneys.ir</code>. Clients still talk to <code>mail.alipourimjourneys.ir</code> for IMAP 993 and SMTP 587. You can log in with the full address, and after a bit of SQL kindness Dovecot also accepts the short local part the way Apple Mail sometimes prefers.</p>
|
||||
<hr>
|
||||
<h2 id="port-25-is-for-servers-port-587-is-for-people">Port 25 is for servers; port 587 is for people</h2>
|
||||
<p>This deserves its own soft lecture because mixing them up is how nice people accidentally build spam infrastructure.</p>
|
||||
<p>Port 25 is the classic loading bay between mail systems. When someone elsewhere sends mail to you, their server connects here. You want Postfix to accept messages addressed <em>to your domain</em>, store them (via Dovecot), and refuse to forward random mail to random destinations for unauthenticated strangers.</p>
|
||||
<p>Port 587 is the counter with the ID check. Humans authenticate. After that, Postfix may relay outward. That is normal. That is how modern clients send.</p>
|
||||
<p>I also turned off AUTH advertisements on port 25. Leaving AUTH enabled there doesn’t instantly mean you’re an open relay, but it does invite bots to spend eternity guessing passwords against a door that shouldn’t even have a doorbell for them. Submission keeps the doorbell. Port 25 keeps the loading bay.</p>
|
||||
<hr>
|
||||
<h2 id="what-an-open-relay-is-and-why-localhost-lies-to-you">What an “open relay” is, and why localhost lies to you</h2>
|
||||
<p>An open relay is a mail server that accepts mail from the internet and forwards it to somewhere else without caring who asked. Spammers adore open relays the way raccoons adore unsecured bins. Modern Postfix defaults are usually cautious, but “usually” is not a control you can put on a checklist and trust with your domain’s reputation.</p>
|
||||
<p>Here’s the trick that wastes afternoons: testing from <code>127.0.0.1</code>.</p>
|
||||
<p>Postfix treats localhost as friendly. Cron jobs, monitoring, local <code>sendmail</code> calls — they need to inject mail without performing a full human login. That friendliness is encoded in <code>mynetworks</code>. So if you point a test tool at loopback, Postfix may accept things it would never accept from a random address on the public internet. You can scare yourself for no reason, or soothe yourself for no reason. Neither is useful.</p>
|
||||
<p>So I tested from the public address instead: pretend to be a stranger, say hello on port 25, try to send mail from <code>evil@example.com</code> to <code>someone@example.com</code>, and stop after the recipient line. The answer I wanted was a firm refusal — something in the spirit of <code>454 Relay access denied</code>. A cheerful <code>250</code> on that recipient line is how you wake up as someone else’s cannon.</p>
|
||||
<p>Kindness to future-you: always ask, “am I testing the door strangers use, or the door my own cron uses?”</p>
|
||||
<hr>
|
||||
<h2 id="dns-but-make-it-human">DNS, but make it human</h2>
|
||||
<h3 id="the-forward-map-name--number">The forward map (name → number)</h3>
|
||||
<p>You already know this one from websites. An A record says “this name has this IPv4.” AAAA says the same for IPv6. For mail you also publish an <strong>MX</strong> record, which is simply a polite note: “if you’re delivering mail for this domain, knock on <em>that</em> hostname.” The hostname still needs A/AAAA records. MX is a pointer with a priority number, not a magical alternate internet.</p>
|
||||
<p>I put <code>mail.alipourimjourneys.ir</code> on funbox for both v4 and v6, pointed MX at that name, and added <code>webmail</code> and <code>mailadmin</code> the same way.</p>
|
||||
<h3 id="why-cloudflares-orange-cloud-is-not-invited">Why Cloudflare’s orange cloud is not invited</h3>
|
||||
<p>Cloudflare’s proxied mode (orange cloud) is wonderful for HTTP websites. It is not a general-purpose “anything TCP” cloak for SMTP and IMAP. If you orange-cloud a mail hostname, you are asking a web reverse proxy to pretend it’s a mail server. That story ends in tears, weird timeouts, or certificates that belong to the wrong plot.</p>
|
||||
<p>Mail hostnames stay <strong>DNS-only</strong> — grey cloud. I already treat some realtime services that way. Mail joins the grey-cloud club and stays there.</p>
|
||||
<hr>
|
||||
<h2 id="ptr-records-the-phonebook-in-reverse">PTR records: the phonebook in reverse</h2>
|
||||
<p>This is the part that confused me the longest the first time I met it years ago, so I’m going to walk slowly.</p>
|
||||
<p>Normal DNS is a phonebook from names to numbers: you look up <code>mail.example.com</code> and learn the IP. <strong>Reverse DNS</strong> is a phonebook from numbers to names. You look up the IP and learn which name claims it. That reverse lookup is a <strong>PTR</strong> record.</p>
|
||||
<p>Those reverse records do <em>not</em> live in your ordinary domain zone on Cloudflare. They live in a special reverse tree managed by whoever owns the IP address space — your hosting people, your ISP, the friendly operator who can edit the reverse zone. Having your own nameservers for <code>alipourimjourneys.ir</code> does not automatically give you the keys to reverse DNS for an IP, any more than owning your apartment gives you the right to rename the street.</p>
|
||||
<p>So you ask the IP owner, kindly and with copy-pasteable values, to publish reverse names that match your mail hostname. In my case both the IPv4 and the IPv6 address should reverse to <code>mail.alipourimjourneys.ir</code>, and that name should forward back to those same addresses. That round trip is sometimes called forward-confirmed reverse DNS. Receiving servers love it because it smells like a real mail host instead of a laptop on hotel Wi‑Fi.</p>
|
||||
<p>You can receive mail without PTR. Sending to big providers without PTR is how you collect mysterious silences and spam-folder exile. In this story, PTR is already in place. The world looks a little less suspicious of funbox. Good.</p>
|
||||
<hr>
|
||||
<h2 id="spf-dkim-and-dmarc-three-different-questions">SPF, DKIM, and DMARC: three different questions</h2>
|
||||
<p>People mash these together into “email authentication.” They’re related, but they answer different questions, and knowing that makes the DNS records feel less like spell ingredients.</p>
|
||||
<p><strong>SPF</strong> answers: “which machines are allowed to send mail claiming this domain?” It’s a TXT record listing your IPs (and sometimes includes of other services). I made mine strict: this server’s v4, this server’s v6, and then <code>-all</code>, which means everyone else should fail SPF. That is a bold stance. Use it when you truly mean “only this box sends.” If you’re mid-migration and scared, a softer <code>~all</code> exists for a reason. I wasn’t mid-migration. I was building one source of truth.</p>
|
||||
<p><strong>DKIM</strong> answers: “was this message signed with our private key, and does the signature still match?” OpenDKIM signs outbound mail. You publish the matching public key under a selector name in DNS. Receivers fetch the key and check the stamp. If someone rewrites the message in silly ways, the stamp breaks. If someone without your key tries to impersonate you, they can’t produce a valid stamp.</p>
|
||||
<p><strong>DMARC</strong> answers: “if SPF/DKIM don’t align properly with the visible From domain, what should receivers do?” You can start with <code>p=none</code> (watch and learn), move to <code>quarantine</code> (bad mail goes to spam), and later <code>reject</code> when you’re brave. I moved to quarantine once signing and SPF looked healthy. Strict alignment settings mean I’m not pretending that “nearby” domains are close enough.</p>
|
||||
<p>Order matters emotionally and technically. Publishing a harsh DMARC before DKIM works is a creative way to punish yourself. Get the notary working. Get the allowed-IP list right. Then tighten the policy.</p>
|
||||
<p>Together they don’t make email “secure” in the Signal sense. They make domain forgery harder and give honest receivers better instincts about your mail. That is already a lot.</p>
|
||||
<hr>
|
||||
<h2 id="building-the-unglamorous-core">Building the unglamorous core</h2>
|
||||
<p>On the OS I installed Postfix and Dovecot the ordinary Ubuntu way, pointed both at a Let’s Encrypt certificate for <code>mail.alipourimjourneys.ir</code>, and configured submission so humans must authenticate on 587. I disabled AUTH on 25. I closed plain IMAP on 143 because I don’t need a cleartext nostalgia port in 2026.</p>
|
||||
<p>Then came OpenDKIM. Generating keys is the easy emotional beat. Publishing the TXT record is easy too. The sneaky beat was the milter socket. Postfix often runs in a chroot under <code>/var/spool/postfix</code>, so the path Postfix uses for the socket is relative to that jail. OpenDKIM must create the socket where Postfix can actually see it, with permissions that let the <code>postfix</code> user connect.</p>
|
||||
<p>I briefly had two <code>Socket</code> lines in <code>opendkim.conf</code>. Config files don’t merge those into a committee. The last one wins. Mine won incorrectly, Postfix looked in the chroot path, found nothing useful, and outbound mail left unsigned while I stared at an empty directory like it had personally betrayed me. One Socket. The correct path. Restart. Log line: <code>DKIM-Signature field added</code>. Instant serotonin.</p>
|
||||
<p>When <code>opendkim-testkey</code> says the key is OK but “not secure,” it is often complaining about DNSSEC validation, not accusing your key of being fake. Read that message gently.</p>
|
||||
<hr>
|
||||
<h2 id="the-matrix-certificate-heist-that-wasnt-a-heist">The Matrix certificate heist (that wasn’t a heist)</h2>
|
||||
<p>Thunderbird once refused my server with a wrong-site story. I opened the certificate details expecting <code>mail.…</code> and found <strong><code>matrix.alipourimjourneys.ir</code></strong> smiling back.</p>
|
||||
<p>No attacker. No cosmic joke from Let’s Encrypt. Just nginx.</p>
|
||||
<p>When a browser or mail client connects with HTTPS or STARTTLS, it names the host it wants (SNI). Nginx chooses a certificate based on that name. If no <code>server_name</code> matches, nginx shrugs and uses the <strong>default</strong> server block. On funbox, that default has long been Matrix. So any half-configured HTTPS name inherits Matrix’s identity and fails the “are you who you say you are?” check in the most confusing way possible.</p>
|
||||
<p>Later I repeated a sibling mistake with <code>webmail.</code>: I enabled an HTTPS site pointing at certificate files that did not exist yet. Nginx then failed its config test. Certbot’s nginx plugin also needs that test to pass. Deadlock. Meanwhile browsers hitting <code>https://webmail.…</code> still fell through to the default server and received Matrix’s cert again. Same villain, new episode.</p>
|
||||
<p>The boring fix is chronological humility. Publish DNS. Put an HTTP-only server block up first. Issue the certificate. Only then flip the site to HTTPS. If you reverse those steps, nginx will teach you about chickens and eggs until you apologize.</p>
|
||||
<hr>
|
||||
<h2 id="from-linux-users-to-real-mailboxes">From Linux users to real mailboxes</h2>
|
||||
<p>The first version used system accounts. Postfix delivered to local Unix users. Dovecot checked passwords with PAM. For a single person it works. It also quietly means your mailbox password is entangled with your login story, and creating “a mail account for a friend” starts to look like creating a Unix inhabitant. That is not the vibe I wanted for a portal.</p>
|
||||
<p>So I moved to virtual mailboxes. PostfixAdmin keeps domains, users, and aliases in Postgres. Dovecot looks up passwords and home directories with SQL. Mail files live under <code>/var/vmail/...</code> owned by a dedicated <code>vmail</code> user. Postfix no longer thinks of those addresses as local Unix people. It thinks of them as virtual destinations and hands the message to Dovecot over LMTP.</p>
|
||||
<p>Why not let Postfix write the files itself? You can, historically. I prefer letting Dovecot be the clerk who already understands Maildir layouts, future filters, and the IMAP view of the world. Postfix remains the traffic cop at the street.</p>
|
||||
<p>One setting decides whether this feels cursed or calm: <code>mydestination</code>.</p>
|
||||
<p>That list means “domains I treat as local Unix delivery.” Virtual domains belong in the virtual maps instead. If your hostname is <code>mail.alipourimjourneys.ir</code> and you also leave that name in <code>mydestination</code>, Postfix may keep trying to deliver like it’s 1999 local mail. Virtual users then appear configured while messages vanish into the wrong conceptual bucket. I set <code>mydestination</code> to localhost and let the mail domain be virtual. Suddenly the model matched reality.</p>
|
||||
<p>PostfixAdmin became the place where I create humans. They can change their own passwords in the user area. I can reset them when someone inevitably forgets. No SSH required for “please add my sibling.”</p>
|
||||
<p>One more client quirk: some apps send only <code>ialipour</code> as the username. The database stores <code>ialipour@mail.…</code>. Dovecot’s “default realm” sounded like it would stitch those together automatically. For PLAIN auth it didn’t save me the way I hoped. Teaching the SQL queries to accept either the full address or <code>local-part@mail.alipourimjourneys.ir</code> did. Also, when authentication fails for “no reason,” consider that you might be typing your Linux password into a mailbox that has its own password now. I will not elaborate. We all learn in private.</p>
|
||||
<hr>
|
||||
<h2 id="webmail-without-losing-the-plot">Webmail without losing the plot</h2>
|
||||
<p>Roundcube is not a second mail server. It’s a website that speaks IMAP and SMTP on your behalf. Same username, same password, same Dovecot, same submission port. I run it in Docker next to PostfixAdmin, with its own database on the mail Postgres.</p>
|
||||
<p>I flirted with serving it under a path like <code>/webmail</code> on the main mail hostname while also serving it at the root of <code>webmail.…</code>. Roundcube has opinions about its base path. If you tell it “you live under <code>/webmail</code>” and then put it at <code>/</code> on another name, it will happily redirect you after login to a URL that 404s inside the container. Apache will even sign the 404 like a polite waiter bringing empty plates.</p>
|
||||
<p>The calm ending: Roundcube lives at <code>https://webmail.alipourimjourneys.ir/</code> as the site root. The portal links there. Path experiments go in the diary under “character development.”</p>
|
||||
<hr>
|
||||
<h2 id="the-bounce-emails-that-taught-me-about-identity">The bounce emails that taught me about identity</h2>
|
||||
<p>After virtualization, funbox started mailing me undeliverable notices. The original messages were those solemn “SECURITY information for funbox” notes that appear when sudo wants a password. Unix likes to mail <code>root</code>. My system identity still thought the local domain was <code>mail.alipourimjourneys.ir</code>, so <code>root</code> became <code>root@mail.alipourimjourneys.ir</code>. That mailbox does not exist on purpose. LMTP rejected it. The bounce returned to me, the sender of the sudo drama. It felt like the house accusing me of breaking the house.</p>
|
||||
<p>The fix is to separate “system mail identity” from “human mail domain.” I set the mailname/origin story back toward localhost for local Unix injection, and I aliased <code>root</code> to my real virtual address so necessary noise still reaches a human. <code>myhostname</code> can still announce itself as <code>mail.…</code> on the SMTP banner. Different knobs, different jobs.</p>
|
||||
<p>Deletes in Thunderbird failing was a softer lesson. Reading worked. Deleting seemed haunted. In IMAP land, “delete” is often “please move this to Trash.” If Trash can’t be created because permissions are wrong, or Dovecot never auto-creates those special folders, the client shrugs and you blame the moon. Giving <code>vmail</code> ownership of the maildirs and letting Dovecot auto-subscribe Trash, Drafts, and Sent restored ordinary mortal behavior.</p>
|
||||
<p>And fail2ban exists because a public mail server’s address is a picnic for scanners. An SSH jail alone is only half a seatbelt. I also watch Postfix and Dovecot auth failures in <code>/var/log/mail.log</code> so repeated guessing gets a timeout instead of an unlimited hobby.</p>
|
||||
<hr>
|
||||
<h2 id="what-done-means-for-me">What “done” means for me</h2>
|
||||
<p>For this build, done means strangers cannot relay through me when I check from the public IP. Port 25 does not offer AUTH as entertainment. Humans submit on 587 with TLS and passwords. IMAP is on 993. SPF is strict, DKIM signs, DMARC quarantines failures. PTR for both families of IP points at <code>mail.alipourimjourneys.ir</code>, and the forward records agree. Cloudflare stays grey for mail names. The mail database is not the Matrix database. Webmail works. Admin works. Thunderbird deletes work. I can explain every line of that paragraph to a tired friend without waving my hands.</p>
|
||||
<p>Would I do it again? Yes. With the same stubborn refusal of the giant appliance, and with a kinder voice toward beginners — including past me — who thought reverse DNS was “just another Cloudflare panel button.”</p>
|
||||
<p>If this story helped you feel less silly for not already knowing what PTR stood for: welcome. That was the point. You’re not a dummy. Email is just a very old building with a lot of labeled doors, and somebody finally walked you through the hallway with the lights on.</p>
|
||||
<hr>
|
||||
<h3 id="links">Links</h3>
|
||||
<ul>
|
||||
<li>Portal: <a href="https://mail.alipourimjourneys.ir">mail.alipourimjourneys.ir</a></li>
|
||||
<li>Webmail: <a href="https://webmail.alipourimjourneys.ir">webmail.alipourimjourneys.ir</a></li>
|
||||
<li>Admin: <a href="https://mailadmin.alipourimjourneys.ir">mailadmin.alipourimjourneys.ir</a></li>
|
||||
</ul>
|
||||
<hr>
|
||||
<div class="signature-block" style="margin-top:1rem">
|
||||
<p><strong>Downloads:</strong>
|
||||
<a href="/sources/posts/self_hosting_mail.md">Markdown</a> ·
|
||||
<a href="/sources/posts/self_hosting_mail.md.asc">Signature (.asc)</a>
|
||||
</p><details class="signature">
|
||||
<summary>View OpenPGP signature</summary>
|
||||
<pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQIzBAABCgAdFiEEVaKl3oR5K6zGyu4/tYgoUOBMjSoFAmpkbukACgkQtYgoUOBM
|
||||
jSo2Yw//UtI5N8jeF+LTvsVHqDbTVyD+x6qiMgRGT4Kpn86V+6NaVjrs6h+kc5Xy
|
||||
TD9gzCfpwsyfSDBGQ2SHM+bOTlyRDfXpH37XhOGVWNymP2guXaQBytJW11N7t6Yq
|
||||
HhcRfnS1ICk+hK1PlZzLroHcxtT7vYWyucSoeGtedufXYVAaHz/mHVY1STMBEebP
|
||||
NvaJqU5PbuHOHICGGtPADKUB8PejmRmUrzWp/bhA6k9muQSa4Bg30GkBLisOPvKq
|
||||
4kgsu5qV7bhB2IyS6nYb+A1QhTD5EcrMzSaqRdNZR0MV2OzW4feSKwBrJqCe5Z8O
|
||||
4BAMhpgk4baTz0+fSjWiKSokQhizXvB6vK21qu2O+5WbkyY/LLi0klLlF2UqhKnU
|
||||
HE3+dYsxSYXMeCMUqDBPSmkxynJYIlUqen1gVt/vrjFpXRs8jsSx+Ec6+nHiwtLu
|
||||
O+8yqHuGOevp91MW9Ly5eXeWMspmEhC4fgBXe4Anpol3FpwkE2neIy6N6D7FSQWM
|
||||
0k4KDrEbfIvoowTRRXmNpHV+ttSYanjzTl3oQQZ+Y9H+g+uPrfh8oUvivrj+2ZOn
|
||||
iv9oMoW6Q3rB7V/2+jptXpswhzfO67MLcGuToI5VIWz7vvOIW7vCAeSBK6LI91iB
|
||||
VrhS5npAuRFTLR/jGboaIsiiAhI3j4zFvgBJ4c4PatN42KODY20=
|
||||
=KCRU
|
||||
-----END PGP SIGNATURE-----
|
||||
</pre>
|
||||
</details><p><em>Verify locally:</em></p>
|
||||
<pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">curl -fSLO https://blog.alipour.eu/sources/posts/self_hosting_mail.md
|
||||
curl -fSLO https://blog.alipour.eu/sources/posts/self_hosting_mail.md.asc
|
||||
gpg --verify self_hosting_mail.md.asc self_hosting_mail.md
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
|
||||
]]></content:encoded></item><item><title>Stealth Trojan VPN Behind Cloudflare Guide</title><link>https://blog.alipour.eu/posts/vpn/</link><pubDate>Tue, 09 Sep 2025 11:05:00 +0200</pubDate><guid>https://blog.alipour.eu/posts/vpn/</guid><description><h2 id="introduction">Introduction</h2>
|
||||
<p>So you want a VPN that <strong>doesn&rsquo;t scream &ldquo;I am a VPN&rdquo;</strong> to every censor and firewall out there?<br>
|
||||
Welcome to the world of <strong>Trojan over WebSocket + TLS behind Cloudflare</strong>.</p>
|
||||
<p>This guide not only shows you how to set it up but also sprinkles in some <strong>debugging magic</strong> so you can figure out why things break (and they <em>will</em> break, trust me).</p>
|
||||
<p>We’ll anonymise domains and secrets, so substitute with your own:</p></description><content:encoded><![CDATA[<h2 id="introduction">Introduction</h2>
|
||||
<p>So you want a VPN that <strong>doesn’t scream “I am a VPN”</strong> to every censor and firewall out there?<br>
|
||||
Welcome to the world of <strong>Trojan over WebSocket + TLS behind Cloudflare</strong>.</p>
|
||||
<p>This guide not only shows you how to set it up but also sprinkles in some <strong>debugging magic</strong> so you can figure out why things break (and they <em>will</em> break, trust me).</p>
|
||||
<p>We’ll anonymise domains and secrets, so substitute with your own:</p>
|
||||
<ul>
|
||||
<li>VPN domain: <code>web.example.com</code></li>
|
||||
<li>Panel domain: <code>panel.example.com</code></li>
|
||||
<li>Secret WS path: <code>/stealth-path_abcd1234</code></li>
|
||||
<li>Password: <code><PASSWORD></code></li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="architecture-at-a-glance">Architecture at a Glance</h2>
|
||||
<p>Think of it as a disguise party:</p>
|
||||
<ul>
|
||||
<li><strong>Trojan</strong> = the shy guest (your VPN protocol)</li>
|
||||
<li><strong>Nginx</strong> = the bouncer checking IDs (reverse proxy)</li>
|
||||
<li><strong>Cloudflare</strong> = the doorman who makes sure nobody sees who’s inside (CDN & proxy)</li>
|
||||
<li><strong>Your fake website</strong> = the mask (camouflage page)</li>
|
||||
</ul>
|
||||
<p>Traffic flow:</p>
|
||||
<pre tabindex="0"><code>Client → Cloudflare (443) → Nginx (443) → Trojan (localhost:54321)
|
||||
</code></pre><hr>
|
||||
<h2 id="step-1-panel-setup-panelexamplecom">Step 1: Panel Setup (<code>panel.example.com</code>)</h2>
|
||||
<ul>
|
||||
<li>Bind the 3x-ui panel to localhost (e.g., <code>127.0.0.1:46309</code>).</li>
|
||||
<li>Choose a funky <strong>web base path</strong> like <code>/panel-bananas_42/</code>.</li>
|
||||
<li>Proxy it through Nginx with HTTPS + Basic Auth.</li>
|
||||
<li>Test it at <code>https://panel.example.com/panel-bananas_42/</code>.</li>
|
||||
</ul>
|
||||
<p><strong>Pro tip:</strong> If you see a blank page → your base path is mismatched or Nginx is eating it. Check logs!</p>
|
||||
<hr>
|
||||
<h2 id="step-2-trojan-inbound">Step 2: Trojan Inbound</h2>
|
||||
<p>In 3x-ui, create a Trojan inbound:</p>
|
||||
<ul>
|
||||
<li>Local port: <code>54321</code></li>
|
||||
<li>Transport: WebSocket</li>
|
||||
<li>Path: <code>/stealth-path_abcd1234</code></li>
|
||||
<li>Security: none</li>
|
||||
<li>Password: <code><PASSWORD></code></li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="step-3-nginx-for-vpn-domain-webexamplecom">Step 3: Nginx for VPN Domain (<code>web.example.com</code>)</h2>
|
||||
<p>Your Nginx is the gatekeeper. Sample config:</p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#66d9ef">server</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">listen</span> <span style="color:#ae81ff">443</span> <span style="color:#e6db74">ssl</span> <span style="color:#e6db74">http2</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">server_name</span> <span style="color:#e6db74">web.example.com</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">ssl_certificate</span> <span style="color:#e6db74">/etc/letsencrypt/live/web.example.com/fullchain.pem</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">ssl_certificate_key</span> <span style="color:#e6db74">/etc/letsencrypt/live/web.example.com/privkey.pem</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># Fake website at root
|
||||
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#f92672">location</span> <span style="color:#e6db74">/</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">root</span> <span style="color:#e6db74">/var/www/html</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">index</span> <span style="color:#e6db74">index.html</span>;
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># Real VPN under secret WS path
|
||||
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#f92672">location</span> <span style="color:#e6db74">/stealth-path_abcd1234</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://127.0.0.1:54321</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_http_version</span> <span style="color:#ae81ff">1</span><span style="color:#e6db74">.1</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Upgrade</span> $http_upgrade;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Connection</span> <span style="color:#e6db74">"upgrade"</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Host</span> $host;
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span></code></pre></div><hr>
|
||||
<h2 id="step-4-firewall-rules">Step 4: Firewall Rules</h2>
|
||||
<p>Lock things down! Only Cloudflare should reach you:</p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ufw allow 22/tcp
|
||||
</span></span><span style="display:flex;"><span>ufw allow 80/tcp
|
||||
</span></span><span style="display:flex;"><span>ufw allow 443/tcp
|
||||
</span></span><span style="display:flex;"><span>ufw deny 54321/tcp
|
||||
</span></span></code></pre></div><hr>
|
||||
<h2 id="step-5-client-config">Step 5: Client Config</h2>
|
||||
<p>Trojan URI:</p>
|
||||
<pre tabindex="0"><code>trojan://<PASSWORD>@web.example.com:443?type=ws&security=tls&host=web.example.com&path=%2Fstealth-path_abcd1234&sni=web.example.com#MyStealthVPN
|
||||
</code></pre><p>iOS clients: Shadowrocket, Stash, FoXray<br>
|
||||
Android clients: v2rayNG, Clash Meta, NekoBox</p>
|
||||
<hr>
|
||||
<h2 id="debugging-time-aka-why-the-heck-doesnt-it-work">Debugging Time (a.k.a. “Why the heck doesn’t it work?!”)</h2>
|
||||
<h3 id="symptom-520-unknown-error-cloudflare">Symptom: <strong>520 Unknown Error (Cloudflare)</strong></h3>
|
||||
<ul>
|
||||
<li>Likely cause: Nginx couldn’t talk to Trojan.</li>
|
||||
<li>Check Nginx error log:
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>tail -n <span style="color:#ae81ff">50</span> /var/log/nginx/error.log
|
||||
</span></span></code></pre></div></li>
|
||||
<li>If you see <code>upstream sent no valid HTTP/1.0 header</code> → you’re mixing HTTPS/HTTP between Nginx and Trojan.</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h3 id="symptom-526-invalid-ssl-certificate">Symptom: <strong>526 Invalid SSL certificate</strong></h3>
|
||||
<ul>
|
||||
<li>Cloudflare → Nginx cert mismatch.</li>
|
||||
<li>Run:
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>openssl s_client -connect web.example.com:443 -servername web.example.com -showcerts
|
||||
</span></span></code></pre></div></li>
|
||||
<li>Make sure CN = <code>web.example.com</code>. If not, fix your cert.</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h3 id="symptom-blank-page-on-panel">Symptom: <strong>Blank page on panel</strong></h3>
|
||||
<ul>
|
||||
<li>Check if the base path matches exactly (case-sensitive, slash-sensitive).</li>
|
||||
<li>Watch for sneaky trailing spaces!</li>
|
||||
<li>Test locally:
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl -s -D- http://127.0.0.1:46309/panel-bananas_42/
|
||||
</span></span></code></pre></div></li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h3 id="symptom-client-wont-connect">Symptom: <strong>Client won’t connect</strong></h3>
|
||||
<ul>
|
||||
<li>Run a WebSocket test:
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl -i -k -H <span style="color:#e6db74">"Connection: Upgrade"</span> -H <span style="color:#e6db74">"Upgrade: websocket"</span> https://web.example.com/stealth-path_abcd1234
|
||||
</span></span></code></pre></div>Expect <code>101 Switching Protocols</code>. If not, check Nginx config.</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h3 id="symptom-censor-still-blocks-you">Symptom: <strong>Censor still blocks you</strong></h3>
|
||||
<ul>
|
||||
<li>Did you expose another service (like SSH, Matrix, or Minecraft) on the same IP?<br>
|
||||
→ They’ll find your origin IP. Use a second VPS or lock those ports down.</li>
|
||||
<li>Did you use an obvious path like <code>/ws</code>?<br>
|
||||
→ Use a random-looking one like <code>/cdn-assets-329df/</code>.</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="pro-tips--fun-tricks">Pro Tips & Fun Tricks</h2>
|
||||
<ul>
|
||||
<li>Serve a fake blog or portfolio at root so your domain looks legit.</li>
|
||||
<li>Rotate WebSocket paths occasionally.</li>
|
||||
<li>Use Cloudflare <strong>Page Rules</strong> to disable Rocket Loader & Minify for your VPN domain.</li>
|
||||
<li>Make friends with your Nginx error.log — it will roast you but it tells the truth.</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="conclusion">Conclusion</h2>
|
||||
<p>By putting Trojan behind Cloudflare, you’ve given your VPN a shiny new disguise:</p>
|
||||
<ul>
|
||||
<li>Looks like normal HTTPS.</li>
|
||||
<li>Hides your origin IP.</li>
|
||||
<li>Forces censors into a tough choice: block Cloudflare (and half the web) or let you pass.</li>
|
||||
</ul>
|
||||
<p>Congrats — you’ve built a VPN with both <strong>style</strong> and <strong>stealth</strong>. 🥷</p>
|
||||
<hr>
|
||||
<div class="signature-block" style="margin-top:1rem">
|
||||
<p><strong>Downloads:</strong>
|
||||
<a href="/sources/posts/vpn.md">Markdown</a> ·
|
||||
<a href="/sources/posts/vpn.md.asc">Signature (.asc)</a>
|
||||
</p><details class="signature">
|
||||
<summary>View OpenPGP signature</summary>
|
||||
<pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQIzBAABCgAdFiEEVaKl3oR5K6zGyu4/tYgoUOBMjSoFAmpkbuAACgkQtYgoUOBM
|
||||
jSo4Yw//T40cakj/BOL/Zh0gxoW4PnH014B7h67Yirq6pB3epUix6bFaZCJnndbD
|
||||
9ZIhmnWMRzbkcA3SVhW1Y3NLpHvhK5UMXNY1qGqrGATQcoVCP7EewhL/3vXgTo5l
|
||||
jFsQFzNxcgOXKKWevuRtL5MY8RuC+PbsfG2ry2jiOtJa9H2UixVJ5E8Imj+VS47O
|
||||
S3XAlxr85O9CEh/TFkS/TuY5P5+VPoOLn6WfdCH5W2IdkW+Vi3bZFJ46LBm6cNBh
|
||||
Iydo+r2msTrqOozimc9hVorPjHZVZLMADJhcsa4pSZ4pDShBYMOZWATQErROPsdl
|
||||
5iyRbmdFb4zWcG5SgjngHnRHFrJ8PVgnFXObb3yZMqA+38RGmRNFgtR0mhMdtKNt
|
||||
QxQDOO/IfO/oNfZzIERUqUnUy/iXs44v8ttTXXE6SkYYoHW335xmDuk8ntrmQA6g
|
||||
YfXO4rJCXxsMaT6RkJaoK5YirKF/9hJYaUYY62SzdfhTmY1TFGGK0+CD+M1kQILC
|
||||
E8pXSfGhaG2bFCzQ+BRMe4o1BXLNjUhvovUgWEBnYTdGF5oXNS1MM29WxD/HC3md
|
||||
d17noEPwOujrtLxEQcAOUcQe02VOfYcX36pbr+ql32hsQMQHZtho1nx5HEGCoCWG
|
||||
3sO7WQTpdBDtkwdHnRJqKBcuWqrVd3YNMwtZE0S6oXVyWkEbB4Y=
|
||||
=IovZ
|
||||
-----END PGP SIGNATURE-----
|
||||
</pre>
|
||||
</details><p><em>Verify locally:</em></p>
|
||||
<pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">curl -fSLO https://blog.alipour.eu/sources/posts/vpn.md
|
||||
curl -fSLO https://blog.alipour.eu/sources/posts/vpn.md.asc
|
||||
gpg --verify vpn.md.asc vpn.md
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
|
||||
]]></content:encoded></item><item><title>Self-Hosting HedgeDoc with Docker + Nginx + Let's Encrypt</title><link>https://blog.alipour.eu/posts/hedge_doc/</link><pubDate>Fri, 29 Aug 2025 00:00:00 +0000</pubDate><guid>https://blog.alipour.eu/posts/hedge_doc/</guid><description><p>HedgeDoc is an open-source collaborative Markdown editor. Think <em>Google Docs for Markdown</em>: multiple people can edit the same note in real-time, with support for diagrams, math, polls, and slide decks. In this post we’ll walk through setting up your own instance on a server, secured with HTTPS.</p>
|
||||
<hr>
|
||||
<h2 id="prerequisites">Prerequisites</h2>
|
||||
<ul>
|
||||
<li>A Linux server with <strong>Docker</strong> and <strong>Docker Compose</strong></li>
|
||||
<li>A domain name pointing to your server (e.g. <code>notes.alipourimjourneys.ir</code>)</li>
|
||||
<li><strong>Nginx</strong> installed for reverse proxying</li>
|
||||
<li><strong>Certbot</strong> for Let’s Encrypt certificates</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="1-create-the-project-directory">1. Create the project directory</h2>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>mkdir ~/hedgedoc <span style="color:#f92672">&amp;&amp;</span> cd ~/hedgedoc</span></span></code></pre></div>
|
||||
<hr>
|
||||
<h2 id="2-create-env">2. Create <code>.env</code></h2>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-env" data-lang="env"><span style="display:flex;"><span>POSTGRES_PASSWORD<span style="color:#f92672">=</span>ChangeThisStrongPassword
|
||||
</span></span><span style="display:flex;"><span>HD_DOMAIN<span style="color:#f92672">=</span>notes.alipourimjourneys.ir</span></span></code></pre></div>
|
||||
<p>Generate a strong password with:</p></description><content:encoded><![CDATA[<p>HedgeDoc is an open-source collaborative Markdown editor. Think <em>Google Docs for Markdown</em>: multiple people can edit the same note in real-time, with support for diagrams, math, polls, and slide decks. In this post we’ll walk through setting up your own instance on a server, secured with HTTPS.</p>
|
||||
<hr>
|
||||
<h2 id="prerequisites">Prerequisites</h2>
|
||||
<ul>
|
||||
<li>A Linux server with <strong>Docker</strong> and <strong>Docker Compose</strong></li>
|
||||
<li>A domain name pointing to your server (e.g. <code>notes.alipourimjourneys.ir</code>)</li>
|
||||
<li><strong>Nginx</strong> installed for reverse proxying</li>
|
||||
<li><strong>Certbot</strong> for Let’s Encrypt certificates</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="1-create-the-project-directory">1. Create the project directory</h2>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>mkdir ~/hedgedoc <span style="color:#f92672">&&</span> cd ~/hedgedoc</span></span></code></pre></div>
|
||||
<hr>
|
||||
<h2 id="2-create-env">2. Create <code>.env</code></h2>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-env" data-lang="env"><span style="display:flex;"><span>POSTGRES_PASSWORD<span style="color:#f92672">=</span>ChangeThisStrongPassword
|
||||
</span></span><span style="display:flex;"><span>HD_DOMAIN<span style="color:#f92672">=</span>notes.alipourimjourneys.ir</span></span></code></pre></div>
|
||||
<p>Generate a strong password with:</p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>openssl rand -base64 <span style="color:#ae81ff">32</span></span></span></code></pre></div>
|
||||
<hr>
|
||||
<h2 id="3-create-docker-composeyml">3. Create <code>docker-compose.yml</code></h2>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">version</span>: <span style="color:#e6db74">"3.9"</span>
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#f92672">services</span>:
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">db</span>:
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">image</span>: <span style="color:#ae81ff">postgres:16</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">environment</span>:
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">POSTGRES_USER</span>: <span style="color:#ae81ff">hedgedoc</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">POSTGRES_PASSWORD</span>: <span style="color:#ae81ff">${POSTGRES_PASSWORD}</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">POSTGRES_DB</span>: <span style="color:#ae81ff">hedgedoc</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">volumes</span>:
|
||||
</span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">db:/var/lib/postgresql/data</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">restart</span>: <span style="color:#ae81ff">unless-stopped</span>
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">hedgedoc</span>:
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">image</span>: <span style="color:#ae81ff">quay.io/hedgedoc/hedgedoc:1.10.2</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">depends_on</span>:
|
||||
</span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">db</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">environment</span>:
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">CMD_DB_URL</span>: <span style="color:#ae81ff">postgres://hedgedoc:${POSTGRES_PASSWORD}@db:5432/hedgedoc</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">CMD_DOMAIN</span>: <span style="color:#ae81ff">${HD_DOMAIN}</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">CMD_PROTOCOL_USESSL</span>: <span style="color:#e6db74">"true"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">CMD_URL_ADDPORT</span>: <span style="color:#e6db74">"false"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">CMD_PORT</span>: <span style="color:#e6db74">"3000"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">CMD_EMAIL</span>: <span style="color:#e6db74">"true"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">CMD_ALLOW_EMAIL_REGISTER</span>: <span style="color:#e6db74">"false"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">volumes</span>:
|
||||
</span></span><span style="display:flex;"><span> - <span style="color:#ae81ff">uploads:/hedgedoc/public/uploads</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">ports</span>:
|
||||
</span></span><span style="display:flex;"><span> - <span style="color:#e6db74">"127.0.0.1:3000:3000"</span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">restart</span>: <span style="color:#ae81ff">unless-stopped</span>
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#f92672">volumes</span>:
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">db</span>:
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#ae81ff">uploads:</span></span></span></code></pre></div>
|
||||
<p>Bring it up:</p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker compose up -d</span></span></code></pre></div>
|
||||
<hr>
|
||||
<h2 id="4-get-a-lets-encrypt-certificate">4. Get a Let’s Encrypt certificate</h2>
|
||||
<p>Request a cert with a <strong>DNS challenge</strong>:</p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo certbot certonly --manual --preferred-challenges dns -d notes.alipourimjourneys.ir -m you@example.com --agree-tos --no-eff-email</span></span></code></pre></div>
|
||||
<p>Add the TXT record certbot asks for, wait for DNS to propagate, then continue.<br>
|
||||
Certificates will be in:</p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>/etc/letsencrypt/live/notes.alipourimjourneys.ir/</span></span></code></pre></div>
|
||||
<hr>
|
||||
<h2 id="5-configure-nginx">5. Configure Nginx</h2>
|
||||
<p>Create <code>/etc/nginx/sites-available/notes.alipourimjourneys.ir</code>:</p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#66d9ef">server</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">server_name</span> <span style="color:#e6db74">notes.alipourimjourneys.ir</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">listen</span> <span style="color:#ae81ff">80</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">listen</span> <span style="color:#e6db74">[::]:80</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">return</span> <span style="color:#ae81ff">301</span> <span style="color:#e6db74">https://</span>$host$request_uri;
|
||||
</span></span><span style="display:flex;"><span>}
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">server</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">server_name</span> <span style="color:#e6db74">notes.alipourimjourneys.ir</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">listen</span> <span style="color:#ae81ff">443</span> <span style="color:#e6db74">ssl</span> <span style="color:#e6db74">http2</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">listen</span> <span style="color:#e6db74">[::]:443</span> <span style="color:#e6db74">ssl</span> <span style="color:#e6db74">http2</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">ssl_certificate</span> <span style="color:#e6db74">/etc/letsencrypt/live/notes.alipourimjourneys.ir/fullchain.pem</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">ssl_certificate_key</span> <span style="color:#e6db74">/etc/letsencrypt/live/notes.alipourimjourneys.ir/privkey.pem</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">location</span> <span style="color:#e6db74">/</span> {
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://127.0.0.1:3000</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Host</span> $host;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Real-IP</span> $remote_addr;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-For</span> $proxy_add_x_forwarded_for;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-Proto</span> <span style="color:#e6db74">https</span>;
|
||||
</span></span><span style="display:flex;"><span>
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_http_version</span> <span style="color:#ae81ff">1</span><span style="color:#e6db74">.1</span>;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Upgrade</span> $http_upgrade;
|
||||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Connection</span> <span style="color:#e6db74">"upgrade"</span>;
|
||||
</span></span><span style="display:flex;"><span> }
|
||||
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
|
||||
<p>Enable the config and reload Nginx:</p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo ln -s /etc/nginx/sites-available/notes.alipourimjourneys.ir /etc/nginx/sites-enabled/
|
||||
</span></span><span style="display:flex;"><span>sudo nginx -t <span style="color:#f92672">&&</span> sudo systemctl reload nginx</span></span></code></pre></div>
|
||||
<hr>
|
||||
<h2 id="6-create-users">6. Create users</h2>
|
||||
<p>Because we disabled self-registration, create accounts manually:</p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker compose exec hedgedoc ./bin/manage_users --add alice@example.com</span></span></code></pre></div>
|
||||
<p>You’ll be prompted for a password.<br>
|
||||
To reset a password later:</p>
|
||||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker compose exec hedgedoc ./bin/manage_users --reset alice@example.com</span></span></code></pre></div>
|
||||
<hr>
|
||||
<h2 id="7-done">7. Done!</h2>
|
||||
<p>Visit <a href="https://notes.alipourimjourneys.ir">https://notes.alipourimjourneys.ir</a> and log in with the user you created. You now have your own collaborative Markdown editor 🎉</p>
|
||||
<hr>
|
||||
<p><strong>Extras:</strong></p>
|
||||
<ul>
|
||||
<li>Backups: dump the PostgreSQL database and save the <code>uploads</code> volume.</li>
|
||||
<li>Upgrades: <code>docker pull quay.io/hedgedoc/hedgedoc:latest && docker compose up -d</code>.</li>
|
||||
<li>Integrations: HedgeDoc supports S3/MinIO image storage, GitHub/GitLab/Google login, and more.</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<div class="signature-block" style="margin-top:1rem">
|
||||
<p><strong>Downloads:</strong>
|
||||
<a href="/sources/posts/hedge_doc.md">Markdown</a> ·
|
||||
<a href="/sources/posts/hedge_doc.md.asc">Signature (.asc)</a>
|
||||
</p><details class="signature">
|
||||
<summary>View OpenPGP signature</summary>
|
||||
<pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQIzBAABCgAdFiEEVaKl3oR5K6zGyu4/tYgoUOBMjSoFAmpkbucACgkQtYgoUOBM
|
||||
jSrPHA//WlMEZx1k/aGx3zau+wRrAOq4XlrvC7keBvqMs0PJGhJmT8jnOMh0+26w
|
||||
AGav2jBuChLYsDx+O8l18uxcsu9fdrz8r4N4sDOnsndSsg15rTT28P3MNvvUL8ns
|
||||
iOc9uEUkxF59rT3OXRI56hH3VX6vzxakdAnW3Afhki2Bl54jur2+6RVtuthGUEV+
|
||||
QZ/36QVXLrOAas1bqq3f38m4M2no3ZqVvpj+Alur2Ji/gcGZlSArBnIoJQoK5L+r
|
||||
UZLJsQ+LrqCJp4i+GkkX05si2srSTjawBu+ssHf+uwPg0Q6AMTbubrGiHrMMtMbM
|
||||
4PCFtS8vD/ZBVkVpSBybyXdMxQU+y9AI1LZ//X4cQWdqgRpinOVENuZ2FeVI6qa/
|
||||
sBGHLThq9nZEXmMT2oQa1wi+CaY17z9fYj20F5moOp2Q6pxBGPyHZRV3WAr5xURU
|
||||
5B9SwVtNqIzm7eoAbwckU3h+TfIJ4vGulSqPqHvZ/XAdbzCVXaSXBN951oA0No1y
|
||||
MyvsIskzKVPvSqndYjxLGiopvOpITgxN5q6a7ubBmxYCrS+SF74jPkDjtc4EFuKB
|
||||
QrMTBm/+Xl/VEESYv5Mx7hwYv1dnmJUFrx62FUYmAMaFP2UcMIlJJ5zQCwsDdREk
|
||||
aG3wFuWH4d9UFohK+MJIHqYk1+TbZJm3bnds8pOqniIZ7M5PcEg=
|
||||
=uf5y
|
||||
-----END PGP SIGNATURE-----
|
||||
</pre>
|
||||
</details><p><em>Verify locally:</em></p>
|
||||
<pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">curl -fSLO https://blog.alipour.eu/sources/posts/hedge_doc.md
|
||||
curl -fSLO https://blog.alipour.eu/sources/posts/hedge_doc.md.asc
|
||||
gpg --verify hedge_doc.md.asc hedge_doc.md
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
|
||||
]]></content:encoded></item></channel></rss>
|
||||
2
public-clearnet/categories/guides/page/1/index.html
Normal file
2
public-clearnet/categories/guides/page/1/index.html
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<!doctype html><html lang=en><head><title>https://blog.alipour.eu/categories/guides/</title>
|
||||
<link rel=canonical href=https://blog.alipour.eu/categories/guides/><meta name=robots content="noindex"><meta charset=utf-8><meta http-equiv=refresh content="0; url=https://blog.alipour.eu/categories/guides/"></head></html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue