Forem là một nền tảng mã nguồn mở để xây dựng cộng đồng hiện đại, độc lập và an toàn. Hiện tại 1 trong số các cộng đồng cho developers là dev.to cũng được xây dựng bằng Forem.

Hoặc mọi người có thể vào đây để tham khảo 1 số cộng đồng đang sử dụng Forem: discover.forem.com

Cá nhân mình thấy thiết kế cũng như chức năng của Forem khá đẹp và thoáng. Nhưng trong quá trình setup nó thì gặp rất nhiều vấn đề, vì trước đây mình chỉ quen dùng wordpress. Bài viết này mình sẽ hướng dẫn chi tiết và những điều lưu ý trong quá trình cài đặt. Hi vọng sẽ giúp ích được cho ai đó đang muốn xây dựng cộng đồng của riêng mình.

Có 1 vài cách cài đặt như sử dung Docker, cài trực tiếp trên Ubuntu, hay sử dụng t2d (Talk to Dokku), nhưng cuối cùng mình chọn cài bằng hướng dẫn của team Forem (được gọi là Self-host) vì nó ổn định, nhiều tài liệu hỗ trợ, được cập nhật thường xuyên.

Lưu ý

  • Forem sẽ được cài đặt trên Fedora CoreOS VM chạy trên một trong số các nhà cung cấp đám mây phổ biến (hiện tại chỗ trợ cho DigitalOcean , AWS và Google Cloud )
  • Mình chọn cài đặt trên DigitalOcean, mọi người có thể cài đặt trên AWS, Google Cloud.
  • Các câu lệnh được thực hiện trên macOS.
  • Cấu hình VPS tối thiểu là 1vCPU, 2GB RAM, vì nó dùng rất nhiều RAM nên cấu hình càng cao càng tốt nhé.

Chuẩn bị

Mở Terminal trên macOS và cài đặt 1 số phần mềm cần thiết

  • Cài đặt HomeBrew:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
  • Cài đặt Git:
brew install git
  • Cài đặt Python 3.x và pip3
brew install python3
  • Cài đặt Ansible: ansible-core 2.11 trở lên (được cung cấp bởi Ansible 4.0.0)
python3 -m pip install --user ansible
python3 -m pip install --user ansible-core==2.12.3
  • Cài đặt Butan:
brew install butane
  • Cài đặt pwgen:
brew install pwgen

Hướng dẫn

1/ Mở Terminal trên macOS để clone bản repo selfhost về máy

git clone https://github.com/forem/selfhost.git
  • Di chuyển vào thư mục selfhost:
cd selfhost

2/ Chạy lệnh sau để tạo tự động 1 số biến:

./setup
  • Sau khi chạy lệnh trên xong, bạn cần lưu lại các giá trị vault kiểu dạng như sau:
vault_secret_key_base: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          31626639326433353437623836636431303161363438396661636130646434396430633032343264
          3433343031316634636133636663666130303330636366350a616333366666656633353136363865
          32333739623836623362343862623963333834656236333433333665666531373534316238633039
          3136396237363839350a633764313365343033623061316364646135356336373062313433383866
          62613738336463366639323230626465353630646161323931396333333764633633303532656632
          33653839363465313863303533613062666364363563353264613439306539366665383462663234
          62313161333566373962396561376166333766366233396533356539393738623666656635373436
          36323064393461393836626537366239363433393261383137366664343734663161323162613634
          35353863356462326435656435373261386230356631396464653937643463323536656538313036
          346661356161386132643837386161376337
Encryption successful
Reading plaintext input from stdin. (ctrl-d to end input, twice if your content does not already have a newline)

vault_imgproxy_key: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          35646264656466303662316162353030343266366562633733623133326663656430356138306266
          6564343661366430383637633433343364363538316633340a336436653162363032646430333861
          33643061636336613361373430396332663964333230626661336637623336666366623839323564
          6432363731363339360a383361323863343131323837636633643261383034316534663634613835
          32316565343937306536343232313530383935386231333830343339653838303533383037616333
          33613732366236653466373233366234646437353166326164313764626439393165333861653538
          61343135373966303931656633363331313838653039626264396136623438626261356632356463
          64643666613930383938373337363238373032323166333730653734353463656139623838313939
          34626539626339353263376231623731656362636666636435366531346232363836613739386464
          303435353334366338646161333636643062
Encryption successful
Reading plaintext input from stdin. (ctrl-d to end input, twice if your content does not already have a newline)

vault_imgproxy_salt: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          32353437333561633733306239333164363165386437313632373761663535373661633261343833
          3534303539333235643530613530323964373530353437330a656636626432336633636132616430
          62316331396436626662303134343964366635316435653264386437653238643964363935643637
          3433623566346265640a343539303166333439626136343336643232643930393261313035313933
          36353833376139306266623261623561373235633432333462323230623665633562333565323235
          36366338646134633738323661656530663261336430633235643938383236353832626138356434
          30663337353235303038336239343934383065613532343137313038643330346436306261666130
          34323137633531393665343564613131343431373835336436656239313738303033333065623964
          30643262313833396234623937616632623561656664393739663266313531366332623434336565
          316634303133623165643138643831373630
Encryption successful
Reading plaintext input from stdin. (ctrl-d to end input, twice if your content does not already have a newline)

vault_forem_postgres_password: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          32316238646635623832303464343262313831326131376662653037633265316166653439346163
          6231366436316431303164323634663137663866303036390a323736346236633835613962613634
          64616233353663643832636435396461393962616264623866613031633931396464346238646564
          3030336139303735640a646133313066383535643763353938663865363361356463623162366234
          37656661613334376361353331343437636633646331366466646130383731613939616639313638
          66613131333735383763656335393762346464346234626633313138376439633965363030616337
          65643663306266623764643732376535336339383334363131636537376531613738653764343865
          39316264633764383264396530393532333639643062333838373531626263623965366462633534
          33343834346333393737353432303065386433303065336563393634393065303838313162653035
          32306565616362346466643366356463656639333162343030323338656532613132303164373134
          343463383266383361303634356133326431
Encryption successful

3/ Thiết lập quyền truy cập SSH cho VPS

Trên Terminal gõ lệnh sau để tạo ssh-key:

ssh-keygen

Để các giá trị mặc định bằng cách nhấn Enter vài lần, kết quả sẽ tương tự như sau:

duyasia@mac-mini selfhost % ssh-keygen
  Generating public/private rsa key pair.
  Enter file in which to save the key (/Users/duyasia/.ssh/id_rsa): 
  Enter passphrase (empty for no passphrase): 
  Enter same passphrase again: 
  Your identification has been saved in /Users/duyasia/.ssh/id_rsa
  Your public key has been saved in /Users/duyasia/.ssh/id_rsa.pub
  The key fingerprint is:
  SHA256:TK5GxqZgf0HQWFnr8IIjRek1TYlCSfqOvOnlacI+MY0 [email protected]
  The key's randomart image is:
  +---[RSA 3072]----+
  |   o=*.*o.       |
  |   o=.* o.       |
  |  ...oo.o        |
  |   o.+ B         |
  |  +o+ B S        |
  | oE*.* +         |
  | .oo= +          |
  |  +=.+           |
  | o=+o            |
  +----[SHA256]-----+

4/ Chỉnh sửa file inventory/forem/setup.yml

(a). Mở file inventory/forem/setup.yml bằng VSCode hoặc Sublime Text và thêm vào giá trị cho các biến Ansible inventory sau:

# CHANGE_REQUIRED — forem_domain_name: example.com
forem_domain_name: REPLACEME
# CHANGE_REQUIRED — default_email: [email protected]
default_email: REPLACEME
forem_subdomain_name: www # can be subdomain, i.e. "community" in community.mainwebsite.com
forem_server_hostname: host # You may change to something else if you choose (i.e. server, srv, etc)

Note: Nếu không thích dùng subdomain thì hãy sửa lại định dạng cho app_domain:

  • Bạn đầu sẽ như này:
app_domain: "{{ forem_subdomain_name }}.{{ forem_domain_name }}"
  • Sửa lại thành:
app_domain: "{{ forem_domain_name }}"

Mình dùng subdomain nên mình sẽ sửa thành như sau (sau khi cài đặt xong thì domain của Forem sẽ là community.duy.asia)

forem_domain_name: duy.asia
default_email: [email protected]
forem_subdomain_name: community
forem_server_hostname: host

(b). Thêm tiếp giá trị cho mỗi biến sau (các giá trị này mình đã lưu lại ở bước 2)

vault_secret_key_base
vault_imgproxy_key
vault_imgproxy_salt
vault_forem_postgres_password
  • Nhớ thụt lề cho các giá trị như sau (thụt vào 1 tab/hoặc 2 dấu cách so với chữ vault_):

(c). Sửa phần ssh_key:

  • Ban đầu sẽ như này:
ssh_key: "{{ lookup('file', '~/.ssh/forem.pub') }}"
  • Sửa thành như này:
ssh_key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"

~/.ssh/id_rsa.pub chính là đường dẫn đến file ssh key mình vừa tạo ở bước 3

5/ Chính sửa file playbooks/providers/digitalocean.yml

Tiếp tục mở file playbooks/providers/digitalocean.yml bằng VSCode hoặc Sublime Text, sửa lại cấu hình VPS tại 2 biến sau:

forem_do_region: nyc3
forem_do_size: s-2vcpu-2gb

Ở đây mình dự định cài trên Droplets có cấu hình 1 vCPU, 2GB RAM (12$/tháng) và datacenter đặt ở Singapore nên mình sẽ sửa thành:

forem_do_region: sgp1
forem_do_size: s-1vcpu-2gb

Với các cấu hình khác, mọi người có thể lấy 1 cách chính xác bằng cách:

  • Trên trang https://cloud.digitalocean.com/
  • Click vào Create > Droplets
  • Chọn cấu hình và region bạn muốn sử dụng
  • Kéo xuống dưới xem tên gợi ý của droplet tại mục hostname. (VD: ubuntu-s-4vcpu-8gb-amd-lon1-01)
  • Như ảnh trên thì:
    • forem_do_region sẽ là lon1 (tức là London)
    • forem_do_size sẽ là s-4vcpu-8gb-amd (tức Premium AMD with NVMe SSD, 8GB / 4 AMD CPUs, 56$/tháng)

6/ Cài đặt DigitalOcean Ansible collection bằng lệnh sau:

ansible-galaxy collection install community.digitalocean

7/ Cài đặt doctl bằng lệnh sau:

brew install doctl

8/ Tạo Personal Access Token trên tài khoản DigitalOcean của bạn

  • Đăng nhập vào DigitalOcean Control Panel.
  • Nhấp vào API trong cột menu bên trái > nó sẽ chuyển đến trang Applications & API > trên tab Tokens/Keys . Trong phần Person access tokens, nhấp vào nút Generate New Token
  • Lưu lại token (dòng dop_v1_49aa…..)

9/ Trên Terminal, gõ vào lệnh sau, sau đó paste access token vừa lưu ở bước 8 vào:

doctl auth init

Nếu bạn đã thêm 1 token trước đây (hoặc token đã hết hạn), bạn có thể thay token mới bằng cách: mở Finder của macOS > nhấn tổ hợp phím Command + Shift + G > nhập vào đường dẫn /Users/<user>/Library/Application Support/doctl (thay <user> bằng user bạn đang dùng trên macOS) > mở file config.yaml > thay đổi access token ở dòng access-token và lưu lại

Test lại token bằng lệnh sau:

doctl account get

Kết quả tương tự như sau là ok

User Email    Team     Droplet Limit  Email Verified  User UUID                             Status
[email protected]   My Team  10             true            89g35441a1c5937f2489c6d821a31614f09   active

10/ Chạy playbook của DigitalOcean để thiết lập Forem

ansible-playbook -i inventory/forem/setup.yml playbooks/providers/digitalocean.yml

DigitalOcean không hỗ trợ Fedora CoreOS nên 1 images tùy chỉnh sẽ được tải về tài khoản của bạn thông qua Ansible. Nếu Wait for fcos-{{ fcos_download_release }} to be created báo time out thì hãy kiểm tra phần Custom Images trên tài khoản DigitalOcean của bạn để xem hình ảnh của bạn có còn ở trạng thái chờ xử lý hay không. Chờ quá trình xử lý hoàn tất và chạy lại lệnh playbook trên.

Nếu tất cả ok thì sẽ log sẽ như này:

11/ Copy IP trên log (hoặc lấy trong droplets mới được tạo trên tài khoản DigitalOcean của bạn) > tạo DNS với record A và gán sub-domain và IP của bạn vào. Ở đây mình dùng DNS của CloudFlare, nên nó sẽ như này:

Nhớ tắt mục Proxy status để tý tạo ssl không bị lỗi

12/ Kết nối vào VPS để chạy 1 lệnh tạo TLS cert

  • Kết nối vào VPS bằng lệnh sau trên Terminal:
ssh core@<IP của droplet>

yes để xác nhận key fingerprint

Kết quả sẽ tương tự như sau:

duyasia@mac-mini % ssh [email protected]
  The authenticity of host '159.223.80.234 (159.223.80.234)' can't be established.
  ED25519 key fingerprint is SHA256:BzNrZH/EJzDHdZAJpGm+QWotVyBU3t9fcRXTkoA2ZsU.
  This key is not known by any other names
  Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
  Warning: Permanently added '159.223.80.234' (ED25519) to the list of known hosts.
  Fedora CoreOS 36.20220703.3.1
  Tracker: https://github.com/coreos/fedora-coreos-tracker
  Discuss: https://discussion.fedoraproject.org/tag/coreos
[core@community (community.duy.asia) ~]$ 
  • Tạo TLS cert bằng lệnh sau:
sudo systemctl restart forem-traefik.service

13/ Chuyển đến tên miền Forem của bạn và tạo tài khoản đầu tiên (tài khoản admin). Xem thêm Admin Documents để biết thêm thông tin về cách thiết lập Forem.

Trên đây là các bước để mình cài đặt Forem (selfhost) trên DigitalOcean. Nếu chỗ nào mình diễn tả khó hiểu thì hãy để lại comment để mình giải thích thêm nhé. Nếu thấy bài viết hữu ích thì hãy share giúp mình để nhiều người cùng biết đến Forem. Thank

Một số lỗi hay gặp

  • Lỗi “Expired SSL Certificate” hoặc “SSL Handshake failed”: Do SSL được tạo bằng Let’s Encrypt nên phải tuân theo Giới hạn chứng chỉ trùng lặp là 5 mỗi tuần. Việc cài đi cài lại Forem nhiều lần dẫn đến lỗi này. Khắc phục bằng cách thay đổi sang sub-domain khác hoặc chờ qua 7 ngày.
  • Khắc phục lỗi SMTP không hoạt động: SMTP được setup và thêm vào tại trang Admin, nhưng nó sẽ không hoạt động luôn, bạn cần kết nối vào vps bằng ssh và chạy lệnh sau để khởi động lại Forem thì nó mới hoạt động: sudo systemctl restart forem.service

Một số command nên biết

Restart để tạo ssl:

sudo systemctl restart forem-traefik.service

Xem log của ‘forem-traefik.service’:

journalctl -u forem-traefik.service > forem_traefik_service_logs.txt
nano forem_traefik_service_logs.txt

systemd: Forem được chạy với các container được cung cấp thông qua Podman và systemd. Các tệp file systemd nằm trong /etc/systemd/system

cd /etc/systemd/system
ls -lah forem*

Forem configs: Tất cả dữ liệu và cấu hình Forem của bạn đều nằm trong đó /opt/forem. Đây là thư mục quan trọng nhất trên Forem của bạn. Bạn nên sao lưu thư mục này thường xuyên.

ll /opt/forem/

Thư mục ‘configs’ chứa tệp cấu hình OpenResty (Nginx) và tệp TOML cấu hình Traefik cùng với tệp acme.json chứa chứng chỉ TLS từ Let’s Encrypt.

ll
  total 4
  -rw-r--r--. 1 root root 2375 Jun 29 17:16 nginx.conf
  drwxr-x---. 2 root root 63 Jun 29 17:16 traefik
ll traefik/
  total 12
  -rw-------. 1 root root 3524 Jun 29 17:20 acme.json
  -rw-r--r--. 1 root root 1928 Jun 29 17:16 dynamic.toml
  -rw-r--r--. 1 root root 740 Jun 29 17:16 traefik.toml

Thư mục ‘data’ chứa các thư mục postgresql, redis và upload của bạn. Thư mục này chứa tất cả nội dung Forem của bạn bao gồm cả các thành viên của bạn.

ll data/
  total 4
  drwx------. 19 polkitd root 4096 Jun 29 17:19 postgresql
  drwxr-x---. 2 polkitd root 28 Jun 29 17:19 redis
  drwxr-x---. 2 core core 6 Jun 29 17:16 uploads

Thư mục ‘envs’ chứa tất cả các tệp biến môi trường định cấu hình các dịch vụ Forem sau:

forem-imgproxy.service với imgproxy.env
forem-postgresql.service với postgresql.env
forem-rails.service, forem-worker.service với rails.env
forem-redis.service với redis.env


Nếu bạn phải tạo cấu hình cho một dịch vụ, bạn có thể chỉnh sửa tệp ENV tương ứng và khởi động lại dịch vụ thông qua systemd. Ví dụ, systemctl restart forem.service sau khi chỉnh sửa ‘rails.env’.

foremctl: Chúng tôi có một tập lệnh trợ giúp (Forem Control) được gọi foremctl. Nó được sử dụng để điều khiển Forem của bạn thông qua CLI.

Cập nhật Forem lên phiên bản mới nhất và khởi động lại

sudo foremctl deploy

foremimg: Có một tập lệnh trợ giúp (Forem Image) được gọi foremimg. Nó được sử dụng để kiểm soát phiên bản Forem của bạn và áp dụng các bản cập nhật. Nó phải được chạy với tư cách là người dùng root.

sudo su

Cập nhật Forem lên phiên bản mới nhất mà không cần khởi động lại

sudo foremimg update

Quay lại báo trước về phiên bản đang chạy cuối cùng và khởi động lại

sudo foremimg rollback
sudo foremctl restart

Cập nhật Fedora CoreOS lên phiên bản ổn định mới nhất

sudo rpm-ostree upgrade --check

Xem trước các bản cập nhật:

sudo rpm-ostree upgrade --preview

Tải xuống các gói mà không cần triển khai chúng:

sudo rpm-ostree upgrade --download-only

Để áp dụng các bản cập nhật và khởi động lại:

sudo rpm-ostree upgrade
systemctl reboot

Sao lưu dữ liệu Forem của bạn

foremctl stop
sudo tar czpf ~core/"$(date '+%Y-%m-%d')-forem-data.tar.gz" /opt/forem
foremctl start

Lưu ý: Chạy foremctl stop sẽ gây ra thời gian chết cho Forem của bạn!

Thay đổi tên miền:

Thay đổi tên miền sẽ là một quá trình khá thủ công mà bạn sẽ phải thực hiện thông qua SSH. Bạn có thể dễ dàng khởi chạy Forem mới với tên miền phù hợp và xóa tên miền cũ nếu bạn không quan tâm đến nội dung bên trong tên miền hiện tại mà bạn đang dùng.

Điều đang được nói ở đây là nơi bạn sẽ phải chỉnh sửa tên miền trên máy chủ hiện tại và các service sẽ cần khởi động lại. Tên miền thử nghiệm của tôi là forum.duy.asia và bạn sẽ cần thay đổi tên miền trong hai tệp này với user root:

sudo su
fgrep forum.duy.asia * -R configs/traefik/dynamic.toml: rule = "Host(forum.duy.asia) && Method(GET, POST, PUT, DELETE, PATCH, HEAD)"envs/rails.env:APP_DOMAIN=forum.duy.asia

Và chạy 2 lệnh sau:

systemctl restart forem.service
systemctl restart forem-traefik.service

Và điều đó sẽ xử lý cài đặt cấu hình phía máy chủ xử lý tên miền. Bạn cần đảm bảo rằng DNS của bạn được trỏ đến máy chủ này và nó phân giải chính xác trước khi bạn khởi động lại forem-traefik.service.

Convert ảnh sang định dạng webp

Bạn cần sửa file imgproxy.env trong thư mục /opt/forem/envs.

Tất cả những gì bạn cần làm là mở file lên và dán dòng sau vào cuối file.

IMGPROXY_ENFORCE_WEBP=true

Sau đó, khởi động lại imgproxy bằng cách chạy mã bên dưới:

sudo systemctl restart forem-imgproxy.service

và khởi động lại rails.

sudo systemctl restart forem-rails.service

Nhận thêm các bài viết chia sẻ?

Để lại email nếu bạn muốn nhận thông tin sớm nhất.