How to get Discourse working in CHINA

Discourse’s official installation guide claims that you can setup Discourse with zero knowledge of Rails or Linux shell thanks to Docker. However, it is only true when you deploy it outside China, due to inevitably unstable network conditions.

tl;dr

Use tun2socks to route all your traffic to international Internet and ./launcher rebuild app --docker-args --net=host --skip-mac-address to build Discourse.

What if you follow official guide

host git fails

The launcher script hangs for quite a while and then fails. It can be fixed by adding to ~/.gitconfig :

[http]
        postBuffer = 157286400
        proxy = socks5://127.0.0.1:1080

container git fails

launcher script creates multiple Docker containers, inside which Discourse’s source code is obtained via git. I met two kinds of error:

Solution:
add sudo -H -E -u discourse git config --global http.proxy socks5://127.0.0.1:3999 to templates/web.template.yml

container yarn and npm fails

Either hangs or too slow until abort lastly.
Solution:
Use privoxy to convert socks5 proxy to http proxy and then add to templates/web.template.yml the following:

su discourse -c 'npm config set proxy http://127.0.0.1:2080'
su discourse -c 'npm config set https-proxy http://127.0.0.1:2080'
su discourse -c 'yarn config set proxy http://127.0.0.1:2080'
su discourse -c 'yarn config set https-proxy http://127.0.0.1:2080'

I also tried to add environment variables to templates/web.template.yml and containers/app.yml:

HTTP_PROXY: http://127.0.0.1:4099
HTTPS_PROXY: http://127.0.0.1:4099

But it fails halfway.

even if you have done this much, it still fails

abort when executing: su discourse -c 'bundle exec rake themes:update assets:precompile'

Ultimate solution

global proxy

This configuration above is extremely tiresome: every time you modify one place, you run launcher, wait several minutes only to end up with failure. The most annoying part is that after so many failed tries it still won’t work.

The ultimate solution is to use tun2socks, which will route all your traffic to international Internet.
I also tried using iptables / nftables, ss-tproxy, and Configure Docker to use a proxy server | Docker Documentation , however Docker container’s traffic still won’t go through proxy.
Here are the steps:

# add tun device and setup route table
ip tuntap add mode tun dev tun0
ip addr add 198.18.0.1/15 dev tun0
ip link set dev tun0 up
ip route del default
ip route add default via 198.18.0.1 dev tun0 metric 1
ip r
# start socks5 to tun converter
tun2socks --device tun0 --proxy socks5://127.0.0.1:5608 -interface enp12s0
# configure sysctl
sysctl net.ipv4.conf.all.rp_filter=0
sysctl net.ipv4.conf.enp12s0.rp_filter=0
# redirect DNS traffic
iptables -t nat -A PREROUTING -p udp --dport 53 -j DNAT --to 8.8.8.8:53;
iptables -t nat -A POSTROUTING -p udp -d 8.8.8.8 --dport 53 -o eth3 -j MASQUERADE;
# check what is your public IP address now 
curl https://ip.lqy.me

./launcher rebuild app

Use --net=host option to make the programs inside the Docker container look as if they are running on the host. Also pass --skip-mac-address option to launcher script otherwise it will also fail.

The full command to build Discourse is:

cd /var/discourse
clone https://github.com/discourse/discourse_docker.git discourse
# modify containers/app.yml or run `./discourse-setup`
./launcher rebuild app  --docker-args --net=host  --skip-mac-address
# if succeed, Discourse will start and listening to 80 / 443 / var/discourse/shared/standalone/nginx.http.sock
# it will auto-start on-boot as long as your docker.service is enabled

You may need a caddy / nginx to do reverse proxy to Discourse’s unix socket.
Here is /etc/caddy/Caddyfile :

lqy.me:443 {
        tls /etc/caddy/ssl/cer /etc/caddy/ssl/key
        # requires `- "templates/web.socketed.template.yml"` to be included in `containers/app.yml`
        reverse_proxy * unix//var/discourse/shared/standalone/nginx.http.sock
        log {
                output file /var/log/caddy/lqy.me.log
        }
}

and /etc/nginx/nginx.conf :

 server {
        listen 0.0.0.0:443 ssl;
        server_name  lqy.me;
        access_log /var/log/nginx/access.lqy.me.log compression;
        error_log /var/log/nginx/error.lqy.me.log;

        ssl_certificate /etc/caddy/ssl/cer;
        ssl_certificate_key /etc/caddy/ssl/key;
        
        ssl_session_timeout 1d;
        ssl_session_tickets off;
        ssl_dhparam /etc/nginx/dhparam;
        ssl_protocols TLSv1.3;
        ssl_ciphers ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
        ssl_prefer_server_ciphers on;
        ssl_stapling on;
        ssl_stapling_verify on;
        # replace with the IP address of your resolver
        #resolver 127.0.0.1;
        # optionally add a HSTS header:
        # add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
        ssl_session_cache shared:MozSSL443:10m;
        location / {
                proxy_pass http://unix:/var/discourse/shared/standalone/nginx.http.sock:;
                proxy_set_header Host $http_host;
                proxy_http_version 1.1;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto $scheme;
                proxy_set_header X-Real-IP $remote_addr;
       }
}