howtos

To help me to remember, and perhaps to help you :)

Updates

Last Update:

2023-07-07

  • Adjusts in the nginx root directories
  • Adjusts in the nginx cache directories

2023-06-19

  • Added Docker config volume mount
  • Added Docker app volume mount

Introduction

This post is a Howto to install Mastodon 4.1.2 using Docker.

We hope you can install Mastodon 4.1.2 using docker like we did.

This is almost the same setup we use to run the instance https://bolha.us.


Information Section

Base Linux System

Ubuntu 20.04 or higher, always.

We're running a Virtual Machine (KVM) inside an open-source Hypervisor.

Hardware size

This proposal uses only docker to handle 500 active users in a 1500 registered server running on a single node (KVM).

  • vpcu: 8 (12 ideal)
  • memory: 12 gb ram (16 gb ideal)
  • network: 500 mbits network minimal (1 gbit ideal)
  • disk: 670 gb

Partition layout

  • root (50g)
  • /var/lib/docker (50g)
  • /var/log (50g)
  • /opt (500g)
  • /tmp (10g)
  • /swapfile (10g)

OPT reserves

  • 250 gb reserved for mastodon upload files
  • 50 gb reserved for elastic
  • 50 gb reserved for postgres
  • 25 gb reserved for redis
  • 25 gb reserved for nginx cache (if in the same server)
  • 100 gb reserved for normal growth of your mastodon instance

Our Baremetal Provider

  • OVH Canada
    • OVH BareMetal ECO
    • Running ProxMox 7.1

We have our own BareMetal Server with ProxMox 7.

We have several Virtual Machines running different Fediverse Tools.

Our VPS Providers

We use OVH/Canada VPS for

  • Load Balancer (NGINX Primary)
  • Video Conference (Our Jitsi instance)

We use VULTR/VPS

  • Load Balancer (NGINX standby)
  • Monitoring (Status Kuma)
  • Other notification services

Other Providers

  • Namecheap to register our domains.
  • CloudFlare to configure and serve DNS Records.
  • Wasabi as our Object Storage/CDN for Mastodon Media
  • BlackBlaze as our Object Storage for Backup

OnPrem Services

  • We're using Uptime Kuma to monitor our instance.
  • We're running our own SMTP Mail Server (Zimbra 8).

PreReqs Section

IPTABLES Config

if you are running nginx on the same machine

  • 22, 80, 443 TCP opened
  • all other traffic blocked on the filter input table

if you are running nginx externally

  • 22 TCP opened
  • 3000 and 4000 TCP only to your NGINX IP
  • all other traffic blocked on the filter input table

in your nginx server (if it's dedicated to mastodon)

  • 22, 80, 443 TCP opened
  • all other traffic blocked on the filter input table

Fail2ban Config

Use it

  • get your port 22 (ssh) protected always

App-armor

First, It's essential to have it up and running continuously.

However, it can cause abnormal behaviors in some scenarios. It's best to keep this component disabled – during the installation, especially if you don't know how to use it or how to configure profiles in case of a problem between docker, mastodon, and app-armor.

It usually won't interfere with the docker or mastodon configuration, but, in case of problems with aa-profiles, we won't cover the solution here. It's best for you to disable it during the installation, and you can re-enable it after, if you want, and know what you are doing.

During the how-to validation, the default Ubuntu app-armor config was enabled, and everything worked fine. However, it's important to mention this in the case of different app-armor configs.


Installation section

1. Installing docker

installing

curl https://get.docker.com | bash

enabling

systemctl enable docker

starting

systemctl start docker

2. Installing docker-compose

installing

curl -s https://api.github.com/repos/docker/compose/releases/latest | grep browser_download_url  | grep docker-compose-linux-x86_64 | cut -d '"' -f 4 | wget -qi -

enabling

chmod +x docker-compose-linux-x86_64

moving to /usr/local/bin, make sure that the dir it's in the PATH var.

mv docker-compose-linux-x86_64 /usr/local/bin/docker-compose

3. Creating directories

main dirs

mkdir -p /opt/mastodon/
mkdir -p /opt/mastodon/{docker,data}

sub-dirs

mkdir -p /opt/mastodon/data/{app,web,database}
mkdir -p /opt/mastodon/data/database/{postgresql,redis,elasticsearch}
mkdir -p /opt/mastodon/data/web/{public,system,config,app}

4. Configuring permissions

creating user and groups

groupadd -g 991 mastodon
useradd mastodon -u 991 -g 991

fixing web perms

chown -R mastodon:mastodon /opt/mastodon/data/web
chown -R mastodon:mastodon /opt/mastodon/data/web/config
chown -R mastodon:mastodon /opt/mastodon/data/web/public
chown -R mastodon:mastodon /opt/mastodon/data/web/system
chown -R mastodon:mastodon /opt/mastodon/data/web/app

fixing database perms

chown -R 1000:1000 /opt/mastodon/data/database/elasticsearch

5. Creating docker config

create the file

vim /opt/mastodon/docker/docker-compose.yml

content

version: '3'

services:
  postgresql:
    image: "postgres:${POSTGRESQL_VERSION}"
    container_name: mastodon_postgresql
    restart: always
    env_file: 
      - database.env
      - versions.env
    shm_size: 256mb
    healthcheck:
      test: ['CMD', 'pg_isready', '-U', 'postgres']
    volumes:
      - postgresql:/var/lib/postgresql/data
    networks:
      - internal_network

  redis:
    image: "redis:${REDIS_VERSION}"
    container_name: mastodon_redis
    restart: always
    env_file: 
      - database.env
      - versions.env
    healthcheck:
      test: ['CMD', 'redis-cli', 'ping']
    volumes:
      - redis:/data
    networks:
      - internal_network

  redis-volatile:
    image: "redis:${REDIS_VERSION}"
    container_name: mastodon_redis_cache
    restart: always
    env_file: 
      - database.env
      - versions.env
    healthcheck:
      test: ['CMD', 'redis-cli', 'ping']
    networks:
      - internal_network

  elasticsearch:
    image: "elasticsearch:${ELASTICSEARCH_VERSION}"
    container_name: mastodon_elastisearch
    restart: always
    env_file: 
      - database.env
      - versions.env
    environment:
      - cluster.name=elasticsearch-mastodon
      - discovery.type=single-node
      - bootstrap.memory_lock=true
      - xpack.security.enabled=true
      - ingest.geoip.downloader.enabled=false
    ulimits:
      memlock:
        soft: -1
        hard: -1
    healthcheck:
      test: ["CMD-SHELL", "nc -z elasticsearch 9200"]
    volumes:
      - elasticsearch:/usr/share/elasticsearch/data
    networks:
      - internal_network

  website:
    image: "tootsuite/mastodon:${MASTODON_VERSION}"
    container_name: mastodon_website
    env_file: 
      - application.env
      - database.env
      - versions.env
    command: bash -c "bundle exec rails s -p 3000"
    restart: always    
    depends_on:
      - postgresql
      - redis
      - redis-volatile
      - elasticsearch
    ports:
      - '3000:3000'
    networks:
      - internal_network
      - external_network
    healthcheck:
      test: ['CMD-SHELL', 'wget -q --spider --proxy=off localhost:3000/health || exit 1']
    volumes:
      - public:/opt/mastodon/public
      - uploads:/opt/mastodon/public/system
      - app:/opt/mastodon/app
      - config:/opt/mastodon/config
       
  streaming:
    image: "tootsuite/mastodon:${MASTODON_VERSION}"
    container_name: mastodon_streaming
    env_file: 
      - application.env
      - database.env
      - versions.env
    command: node ./streaming
    environment:
      - DB_POOL=4
    restart: always
    depends_on:
      - postgresql
      - redis
      - redis-volatile
      - elasticsearch
    ports:
      - '4000:4000'
    networks:
      - internal_network
      - external_network
    healthcheck:
      test: ['CMD-SHELL', 'wget -q --spider --proxy=off localhost:4000/api/v1/streaming/health || exit 1']
    volumes:
      - public:/opt/mastodon/public
      - uploads:/opt/mastodon/public/system
      - app:/opt/mastodon/app
      - config:/opt/mastodon/config


  sidekiq:
    image: "tootsuite/mastodon:${MASTODON_VERSION}"
    container_name: mastodon_sidekiq
    env_file: 
      - application.env
      - database.env
      - versions.env
    restart: always
    depends_on:
      - postgresql
      - redis
      - redis-volatile
      - website
    networks:
      - internal_network
      - external_network
    environment:
      - DB_POOL=18
    healthcheck:
      test: ['CMD-SHELL', "ps aux | grep '[s]idekiq\ 6' || false"]
    command: bundle exec sidekiq -c 18
    volumes:
      - public:/opt/mastodon/public
      - uploads:/opt/mastodon/public/system
      - app:/opt/mastodon/app
      - config:/opt/mastodon/config

  shell:
    image: "tootsuite/mastodon:${MASTODON_VERSION}"
    env_file: 
      - application.env
      - database.env
      - versions.env
    command: /bin/bash 
    restart: "no"
    networks:
      - internal_network
      - external_network
    volumes:
      - public:/opt/mastodon/public
      - uploads:/opt/mastodon/public/system
      - app:/opt/mastodon/app
      - config:/opt/mastodon/config

networks:
  external_network:
  internal_network:
    internal: true

volumes:
  postgresql:
    driver_opts:
      type: none
      device: /opt/mastodon/data/database/postgresql
      o: bind    
  redis:
    driver_opts:
      type: none
      device: /opt/mastodon/data/database/redis
      o: bind    
  elasticsearch:
    driver_opts:
      type: none
      device: /opt/mastodon/data/database/elasticsearch
      o: bind    
  uploads:
    driver_opts:
      type: none
      device: /opt/mastodon/data/web/system
      o: bind
  app:
    driver_opts:
      type: none
      device: /opt/mastodon/data/web/app
      o: bind
  config:
    driver_opts:
      type: none
      device: /opt/mastodon/data/web/config
      o: bind
  public:
    driver_opts:
      type: none
      device: /opt/mastodon/data/web/public
      o: bind

6. Env Files

6.1 versions.env

create the versions.env file

vim /opt/mastodon/docker/versions.env

content

MASTODON_VERSION=v4.1.2
POSTGRESQL_VERSION=14
ELASTICSEARCH_VERSION=7.17.10
REDIS_VERSION=7

creating a symbolic link to load the versions properly

cd /opt/mastodon/docker; ln -s versions.env .env

6.2 application.env

create the application.env file

vim /opt/mastodon/docker/application.env

content

# environment config

RAILS_ENV=production
NODE_ENV=production

# web performance/tuning/concurrency

WEB_CONCURRENCY=2
MAX_THREADS=4

# locale config

DEFAULT_LOCALE=en

# local domain of your instance

LOCAL_DOMAIN=dev.bolha.us

# redirect to the first profile?

SINGLE_USER_MODE=false

# rails will serve static files?

RAILS_SERVE_STATIC_FILES=true

# email config

SMTP_SERVER=smtp.provider.tld
SMTP_PORT=587
SMTP_LOGIN=mastodon@provider.tld
SMTP_AUTH_METHOD=plain
SMTP_FROM_ADDRESS=mastodon@provider.tld
SMTP_PASSWORD=change_this_password_to_the_real_one

# secrets

SECRET_KEY_BASE=YOU_WILL_GENERATE_AND_REPLACE_HERE_LATER_CONTINUE_THE_DOC
OTP_SECRET=YOU_WILL_GENERATE_AND_REPLACE_HERE_LATER_CONTINUE_THE_DOC

# web push

VAPID_PRIVATE_KEY=YOU_WILL_GENERATE_AND_REPLACE_HERE_LATER_CONTINUE_THE_DOC
VAPID_PUBLIC_KEY=YOU_WILL_GENERATE_AND_REPLACE_HERE_LATER_CONTINUE_THE_DOC

6.3 database.env

create the database.env file

vim /opt/mastodon/docker/database.env

content

# postgresql config

POSTGRES_HOST=postgresql
POSTGRES_USER=mastodon
POSTGRES_DB=mastodon_production
POSTGRES_PASSWORD=you_will_change_this_password_ahead_on_this_doc

# elasticsearch config

ES_JAVA_OPTS="-Xms1024m -Xmx2048m"
ELASTIC_PASSWORD=you_will_change_this_password_ahead_on_this_doc

# redis config

REDIS_HOST=redis
REDIS_PORT=6379
REDIS_URL=redis://redis:6379

# redis cache config

CACHE_REDIS_HOST=redis-volatile
CACHE_REDIS_PORT=6379
CACHE_REDIS_URL=redis://redis-volatile:6379

# postgresql mastodon integration

DB_HOST=postgresql
DB_USER=mastodon
DB_NAME=mastodon_production
DB_PASS=you_will_change_this_password_ahead_on_this_doc
DB_PORT=5432

# elasticsearch mastodon integration

ES_ENABLED=true
ES_HOST=elasticsearch
ES_PORT=9200
ES_USER=elastic
ES_PASS=you_will_change_this_password_ahead_on_this_doc

7. generating secrets and passwords

7.1 secret key and OTP secret

Now we need to generate two secrets, SECRETKEYBASE and OTP_SECRET.

From the docker host run

$ docker-compose -f /opt/mastodon/docker/docker-compose.yml run --rm shell bundle exec rake secret

Yes, you need to run two times, one for each secret and then update the application.env file.

7.2 generate vapid secrets

Now we need to generate the VAPID secrets, VAPIDPRIVATEKEY and VAPIDPUBLICKEY.

From the docker host run

$ docker-compose -f /opt/mastodon/docker/docker-compose.yml run --rm shell bundle exec rake mastodon:webpush:generate_vapid_key

Get the output and append the file application.env

7.3 generate elasticsearch password

Generate a password for the var ELASTIC_PASSWORD

$ openssl rand -hex 15

Update the database.env file, don't forget to update ES_PASS, it's the same password.

7.4 generate postgresql password

Generate a password for the var POSTGRES_PASSWORD

$ openssl rand -hex 15

Update the database.env file.


8. configuring a local nginx to serve mastodon

Here we'll configure an NGINX on the same docker host of our mastodon.

8.1 extracting the static files for nginx cache

This procedure is only used by NGINX for CACHE purposes, mastodon do not use this static files, it's used by NGINX, and NGINX Only.

let's create a docker volume in YOUR NGINX SERVER to store the static mastodon files

$ mkdir -p /opt/www/mastodon/dev.bolha.us/public/4.1.2
$ chown -R mastodon:mastodon /opt/www/mastodon
$ docker volume create --opt type=none --opt device=/opt/www/mastodon/dev.bolha.us/public/4.1.2 --opt o=bind mastodon_public_4.1.2

now let's copy the files from the container to the device

$ docker run --rm -v "mastodon_public_4.1.2:/static" tootsuite/mastodon:v4.1.2 bash -c "cp -r /opt/mastodon/public/* /static/"

nice, check if the files were copied properly

$ ls /opt/www/mastodon/dev.bolha.us/public/4.1.2

output expected

500.html  avatars    embed.js  favicon.ico  inert.css  oops.gif  packs       sounds  sw.js.map                 web-push-icon_favourite.png
assets    badge.png  emoji     headers      ocr        oops.png  robots.txt  sw.js   web-push-icon_expand.png  web-push-icon_reblog.png

now we can remove our docker volume; we only needed the volume for the copy.

$ docker volume rm mastodon_public_4.1.2

ok, now we have a directory to be used by nginx cache to serve the static files.

8.2 Creating directories

$ mkdir -p /opt/nginx/{docker,html,vhost,conf,stream,certbot,cache,logs}
$ mkdir -p /opt/nginx/certbot/{html,conf}
$ mkdir -p /opt/nginx/cache/public/4.1.2

8.3 Creating nginx.conf

Let's create our nginx.conf

$ vim /opt/nginx/conf/nginx.conf

The contents of the file

user nginx;
worker_processes auto;

pid        /var/run/nginx.pid;

include /etc/nginx/conf.d/*.conf;

events {
	worker_connections 2048;
}

http {

	sendfile on;
	tcp_nopush on;
	tcp_nodelay on;

	keepalive_timeout 65;
	types_hash_max_size 2048;

	include /etc/nginx/mime.types;
	default_type application/octet-stream;

	ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
	ssl_prefer_server_ciphers on;
	
	log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

	access_log /var/log/nginx/access.log;
	error_log /var/log/nginx/error.log;

	gzip on;

	include /etc/nginx/vhosts/*.conf;
	include /etc/nginx/sites-enabled/*;

}

stream {

 log_format proxy '$remote_addr [$time_local] '
                 '$protocol $status $bytes_sent $bytes_received '
                 '$session_time "$upstream_addr" '
                 '"$upstream_bytes_sent" "$upstream_bytes_received" "$upstream_connect_time" "$upstream_addr"';

   include /etc/nginx/stream/*.conf;

}

8.4 Creating default.conf

Let's create our default.conf to avoid nginx container default configs conflict.

$ vim /opt/nginx/vhost/default.conf

The contents of the file

server {
    listen       80;
    server_name  localhost;

    location /.well-known/acme-challenge/ {
      root /var/www/certbot;
    }
  
    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

}

8.5 Creating dev.bolha.us.conf

Now we can create the config of our mastodon instance; we will use dev.bolha.us as our domain, just as an example.

$ vim /opt/nginx/vhost/dev-bolha-us.conf

With this content

# connection configuration

map $http_upgrade $connection_upgrade {
  default upgrade;
  ''      close;
}

# upstream configuration

upstream backend {
    server localhost:3000 fail_timeout=0;
}

upstream streaming {
    server localhost:4000 fail_timeout=0;
}

# cache for static files

proxy_cache_path /var/cache/mastodon/public/4.1.2 levels=1:2 keys_zone=MASTODON_CACHE_v412:10m inactive=7d max_size=3g;

# server configuration

server {
  listen 80;
  server_name dev.bolha.us;

  location /.well-known/acme-challenge/ {
      root /var/www/certbot;
  }
  
  location / { 
    return 301 https://dev.bolha.us$request_uri; 
  }
  
}

server {
  listen 443 ssl http2;
  server_name dev.bolha.us;

  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_prefer_server_ciphers on;
  ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';

  ssl_session_cache shared:SSL:10m;
  ssl_session_tickets off;

  access_log /var/log/nginx/mastodon-dev-bolha-us-access.log;
  error_log /var/log/nginx/mastodon-dev-bolha-us-error.log;

  ssl_certificate /etc/letsencrypt/live/bolha.us/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/bolha.us/privkey.pem;

  keepalive_timeout    70;
  sendfile             on;
  client_max_body_size 80m;

  root /var/www/mastodon/dev.bolha.us/public/4.1.2;

  gzip on;
  gzip_disable "msie6";
  gzip_vary on;
  gzip_proxied any;
  gzip_comp_level 6;
  gzip_buffers 16 8k;
  gzip_http_version 1.1;
  gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml image/x-icon;

  add_header Strict-Transport-Security "max-age=31536000" always;

  location / {
    try_files $uri @proxy;
  }

  location ~ ^/(system/accounts/avatars|system/media_attachments/files) {
    add_header Cache-Control "public, max-age=31536000, immutable";
    add_header Strict-Transport-Security "max-age=31536000" always;
    root /opt/mastodon/;
    try_files $uri @proxy;
  }

  location ~ ^/(emoji|packs) {
    add_header Cache-Control "public, max-age=31536000, immutable";
    add_header Strict-Transport-Security "max-age=31536000" always;
    try_files $uri @proxy;
  }

  location /sw.js {
    add_header Cache-Control "public, max-age=0";
    add_header Strict-Transport-Security "max-age=31536000" always;
    try_files $uri @proxy;
  }

  location @proxy {
    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_set_header Proxy"";
    proxy_pass_header Server;

    proxy_pass http://backend;
    proxy_buffering on;
    proxy_redirect off;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;

    proxy_cache MASTODON_CACHE_v412;
    proxy_cache_valid 200 7d;
    proxy_cache_valid 410 24h;
    proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
    add_header X-Cached $upstream_cache_status;
    add_header Strict-Transport-Security "max-age=31536000" always;

    tcp_nodelay on;
  }

  location /api/v1/streaming {
    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_set_header Proxy"";

    proxy_pass http://streaming;
    proxy_buffering off;
    proxy_redirect off;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;

    tcp_nodelay on;
  }

  error_page 500 501 502 503 504 /500.html;
}

What do you need to change here?

  • server_name dev.bolha.us;
  • return 301 https://dev.bolha.us$request_uri;
  • server_name dev.bolha.us;
  • access_log /var/log/nginx/mastodon-dev-bolha-us-access.log;
  • error_log /var/log/nginx/mastodon-dev-bolha-us-error.log;
  • ssl_certificate /etc/letsencrypt/live/bolha.us/fullchain.pem;
  • sslcertificatekey /etc/letsencrypt/live/bolha.us/privkey.pem;
  • root /var/www/mastodon/dev.bolha.us/public/4.1.2;

To use this config, we expect that you have it before starting your nginx

  • the directory root with the static files
  • the ssl certificates generated already

Wait and follow the instructions carefully, baby steps!

8.6 Creating the nginx docker-compose configuration

After that, we can create the docker-compose file.

cd /opt/nginx/docker
vim docker-compose.yml

here are the contents of the file

version: '3'

services:
  nginx:
    image: nginx:latest
    container_name: nginx
    restart: always
    network_mode: host
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /opt/nginx/conf/nginx.conf:/etc/nginx/nginx.conf
      - nginx_log:/var/log/nginx
      - nginx_vhost:/etc/nginx/vhosts
      - nginx_stream:/etc/nginx/stream
      - nginx_html:/usr/share/nginx/html
      - nginx_cache:/var/cache/mastodon
      - certbot_conf:/etc/letsencrypt
      - certbot_html:/var/www/certbot
      - mastodon_public:/var/www/mastodon
    healthcheck:
      test: ["CMD-SHELL", "wget -O /dev/null http://localhost || exit 1"]
      timeout: 10s
  certbot:
    image: certbot/certbot:latest
    restart: no
    container_name: certbot
    network_mode: host
    volumes:
      - certbot_conf:/etc/letsencrypt
      - certbot_html:/var/www/certbot

volumes:
  mastodon_public:
    driver_opts:
      type: none
      device: /opt/www/mastodon
      o: bind
  nginx_log:
    driver_opts:
      type: none
      device: /opt/nginx/logs
      o: bind
  nginx_cache:
    driver_opts:
      type: none
      device: /opt/nginx/cache
      o: bind
  nginx_vhost:
    driver_opts:
      type: none
      device: /opt/nginx/vhost
      o: bind
  nginx_stream:
    driver_opts:
      type: none
      device: /opt/nginx/stream
      o: bind
  nginx_html:
    driver_opts:
      type: none
      device: /opt/nginx/html
      o: bind
  certbot_conf:
    driver_opts:
      type: none
      device: /opt/nginx/certbot/conf
      o: bind
  certbot_html:
    driver_opts:
      type: none
      device: /opt/nginx/certbot/html
      o: bind

8.7 Creating the letsencrypt certificate

let's create the certificate first. It would be best to ensure that your domain points to your nginx docker server or cerbot will fail during the certificate generation.

$ cd /opt/nginx/docker
$ docker-compose run --rm certbot certonly --webroot --webroot-path /var/www/certbot/ -d dev.bolha.us

add the command to renew to your crontab

$ crontab -e

add this command

00 3 * * * docker-compose /opt/nginx/docker/docker-compose.yml run --rm certbot renew

8.8 Starting NGINX

now let's run our nginx :)

$ cd /opt/nginx/docker
$ docker-compose -up -d

check if your nginx is running accordingly

$ docker ps
$ docker logs -f


9. Configuring an external NGINX dedicated to mastodon

Point the domain to the external nginx server.

Follow section 8 in your NGINX Server.|

The static files are only needed by NGINX, you do not need to do that part on the mastodon server.

Open the ports 3000 and 4000 TCP for the NGINX IP on your Mastodon Server.


10. Configuring an existing (external) non-docker NGINX

Follow the instructions from the sections:

  • 8.1
  • 8.5

You need to create the cache directory for nginx

proxy_cache_path /var/cache/mastodon/public/4.1.2 levels=1:2 keys_zone=MASTODON_CACHE_v412:10m inactive=7d max_size=3g;

You need to generate your certs.

You need to adjust to having it running in your setup, we don't know your structure, so we can't cover this here, sorry.


11. Starting your Mastodon (Finally, right? :)

If you get here, you're already a warrior :)

Let's do it!

Go to your mastodon server docker config directory.

$ cd /opt/mastodon/docker

11.1 pulling the images

pulling

$ docker-compose pull

output expected

 ✔ postgresql Pulled                                                                                
 ✔ redis  Pulled
 ✔ redis-volatile Skipped - Image is already being pulled by redis                                                                                                     
 ✔ elasticsearch Pulled
 ✔ website Pulled                                                                                                                                                    
 ✔ streaming Skipped - Image is already being pulled by website                                                                                                       
 ✔ sidekiq Skipped - image is already being pulled by website
✔ shell Skipped - Image is already being pulled by website

11.2 starting postgresql and redis

starting databases

$ docker-compose up -d postgresql redis redis-volatile

output expected

 ✔ Network docker_internal_network  Created                                                                                                                            0.0s
 ✔ Network docker_external_network  Created                                                                                                                            0.0s
 ✔ Volume "docker_elasticsearch"    Created                                                                                                                            0.0s
 ✔ Volume "docker_public"           Created                                                                                                                            0.0s
 ✔ Volume "docker_uploads"          Created                                                                                                                            0.0s
 ✔ Volume "docker_app"              Created                                                                                                                            0.0s
 ✔ Volume "docker_postgresql"       Created                                                                                                                            0.0s
 ✔ Volume "docker_redis"            Created                                                                                                                            0.0s
 ✔ Container mastodon_redis         Started                                                                                                                            0.5s
 ✔ Container mastodon_redis_cache   Started                                                                                                                            0.5s
 ✔ Container mastodon_postgresql    Started                                                                                                                            0.5s                                                                                                                      

11.2 running the database setup

$ docker-compose run --rm shell bundle exec rake db:setup

output expected

Database 'mastodon_production' already exists

11.3 starting remaining services

$ docker-compose up -d

output expected

+] Running 8/8
 ✔ Container mastodon_elastisearch  Started                                                                                                                                                                                                                                            1.1s
 ✔ Container mastodon_website       Started                                                                                                                            1.6s
 ✔ Container mastodon_streaming     Started                                                                                                                            1.6s
 ✔ Container mastodon_sidekiq       Started                                                                                                                            2.2s

11.4 Checking everything

let's verify our containers

root@dev:/opt/nginx/docker# docker ps

output expected

CONTAINER ID   IMAGE                       COMMAND                  CREATED          STATUS                            PORTS                                NAMES
ac17eef8d384   nginx:latest "/docker-entrypoint.…" 6 seconds ago    Up 5 seconds (health: starting)                                        mastodon_nginx
adabce4171e2   tootsuite/mastodon:v4.1.2 "/usr/bin/tini -- bu…" 25 minutes ago   Up 25 minutes (healthy)           3000/tcp, 4000/tcp                   mastodon_sidekiq
47d8b9720fc4   tootsuite/mastodon:v4.1.2   "/usr/bin/tini -- ba…"   25 minutes ago   Up 25 minutes (healthy)           127.0.0.1:3000->3000/tcp, 4000/tcp   mastodon_website
f55bea899b31   tootsuite/mastodon:v4.1.2   "/usr/bin/tini -- no…"   25 minutes ago   Up 25 minutes (healthy)           3000/tcp, 127.0.0.1:4000->4000/tcp   mastodon_streaming
e2ffd239c210   redis:7 "docker-entrypoint.s…" 25 minutes ago   Up 25 minutes (healthy)                                                mastodon_redis_cache
3914fc3f784b   postgres:14 "docker-entrypoint.s…" 25 minutes ago   Up 25 minutes (healthy)                                                mastodon_postgresql
d02fffd8f108   elasticsearch:7.17.10       "/bin/tini -- /usr/l…"   25 minutes ago   Up 25 minutes (healthy)                                                mastodon_elastisearch
e500590b9a20   redis:7 "docker-entrypoint.s…" 25 minutes ago   Up 25 minutes (healthy)                                                mastodon_redis

11.5 Enabling registration with approval

$ cd /opt/mastodon/docker
$ docker-compose run --rm shell bin/tootctl settings registrations approved

12. Creating users via terminal

Creating an owner

$ docker-compose run --rm shell bin/tootctl accounts create gutocarvalho --email gutocarvalho@bolha.us --confirmed --role Owner
$ docker-compose run --rm shell bin/tootctl accounts approve gutocarvalho

Creating an admin

$ docker-compose run --rm shell bin/tootctl accounts create joseaugusto --email joseaaugusto@bolha.us --confirmed --role Admin
$ docker-compose run --rm shell bin/tootctl accounts approve joseaugusto

Creating an moderator

$ docker-compose run --rm shell bin/tootctl accounts create augustocarvalho --email augustocarvalho@bolha.us --confirmed --role Moderator
$ docker-compose run --rm shell bin/tootctl accounts approve augustocarvalho

Creating a normal user

$ docker-compose run --rm shell bin/tootctl accounts create arturcarvalho --email arturcarvalho@bolha.us --confirmed
$ docker-compose run --rm shell bin/tootctl accounts approve arturcarvalho

13. Accessing your mastodon!

Go to your mastodon!

https://dev.bolha.us

It's ready and should work, you just need to login.


14. Creating maintenance tasks using crontab

open your root crontab

$ crontab -e

indexing data of elasticsearch to create the 'https://dev.bolha.us/explore' content

00 */6 * * * docker-compose -f /opt/mastodon/docker/docker-compose.yml run --rm shell tootctl search deploy --concurrency 4

this will recount all accounts numbers of the instance daily

30 1 * * * docker-compose -f /opt/mastodon/docker/docker-compose.yml run --rm shell tootctl cache recount accounts --concurrency 4

this will force all users to follow special instance accounts

00 2 * * * docker-compose -f /opt/mastodon/docker/docker-compose.yml run --rm shell tootctl accounts follow status

00 2 * * * docker-compose -f /opt/mastodon/docker/docker-compose.yml run --rm shell tootctl accounts follow news

00 2 * * * docker-compose -f /opt/mastodon/docker/docker-compose.yml run --rm shell tootctl accounts follow tips

00 2 * * * docker-compose -f /opt/mastodon/docker/docker-compose.yml run --rm shell tootctl accounts follow gutocarvalho

this will clean media from external instances in our local cache, it will clean everything older than 15 days in the cache

30 2 * * * docker-compose -f /opt/mastodon/docker/docker-compose.yml run --rm shell tootctl media remove --days=15 --concurrency 4

this will clean local thumbnails for preview cards in our local cache; it will clean everything older than 15 days in the cache

00 3 * * * docker-compose -f /opt/mastodon/docker/docker-compose.yml run --rm shell tootctl preview_cards remove --days=15 --concurrency 4

this will regenerate all user feeds every Sunday

00 1 * * 0 docker-compose -f /opt/mastodon/docker/docker-compose.yml run --rm shell tootctl feeds clear
    
00 2 * * 0 docker-compose -f /opt/mastodon/docker/docker-compose.yml run --rm shell tootctl feeds build --concurrency 4

this will remove statuses without users/references every Sunday

00 3 * * 0 docker-compose -f /opt/mastodon/docker/docker-compose.yml run --rm shell tootctl statuses remove --days 60

References!


15. Maintenance tasks

Remember to add this before the command; you will use tootctl inside the mastodon_shell container

$ docker-compose -f /opt/mastodon/docker/docker-compose.yml run --rm shell COMMAND

Example

$ docker-compose -f /opt/mastodon/docker/docker-compose.yml run --rm shell tootctl accounts modify user@domain.tld --approve

I'll remove the first part of the command to make the examples easily understood.

15.1 Accounts Tasks

Reset password

$ tootctl accounts modify user@domain.tld --reset-password

Disable user

$ tootctl accounts modify user@domain.tld --disable

Approve user

$ tootctl accounts modify user@domain.tld --approve

Disable 2FA in case someone forgets the 2FA code or device

$ tootctl accounts modify user@domain.tld --disable-2fa

If you are not seeing user data from a specific user or domain

$ tootctl accounts refresh user@domain.tld

Delete

$ tootctl accounts delete user@domain.tld

15.2 Other tasks

If you are not seeing images from a specific domain

$ tootctl media refresh domain.tld --concurrency 4

Remove all accounts from a given DOMAIN without leaving behind any records. Unlike a suspension, if the DOMAIN still exists in the wild, it means the accounts could return if they are resolved again.

$ tootctl domains purge domain.tld --concurrency 4

Remove remote accounts that no longer exist. Queries every single remote account in the database to determine if it still exists on the origin server, and if it doesn't, then remove it from the database.

Accounts with confirmed activity within the last week are excluded from the checks, in case the server is down.

$ tootctl accounts cull domain.tld --concurrency 4

16. Backup

You can use whatever you want to protect your data; we'll focus on what you need to do before the backup and what you need to backup to have your data replicated to a safe location.

16.1 Cold Backup

  1. stop all containers

  2. run a backup of your /opt/mastodon, /opt/nginx, and /opt/www

16.2 Hot Backup

  1. run a backup of your postgresql

  2. run a backup of your /opt/mastodon/data/web directory

  3. run a backup of your /opt/mastodon/docker directory

  4. run a backup of your /opt/nginx directory

16.3 Our Backup Provider

Our automation sends our backup to blackblaze object storage, it's a good and cheap provider; we do full backups of pgsql and configs every day.


17. API Information

You cant get some info from the API using curl and jq

$ curl -s https://dev.bolha.us/api/v1/instance | jq
$ curl -s https://dev.bolha.us/api/v2/instance | jq

Let's create a script to get some information from mastodon:

    $ vim mastodon_api_info.sh

Script Content

#!/bin/bash

INSTANCE_ENDPOINT_V1="https://dev.bolha.us/api/v1/instance"
INSTANCE_ENDPOINT_V2="https://dev.bolha.us/api/v2/instance"
INSTANCE_URL="https://dev.bolha.us"

COUNT_TOTAL_USERS=$(curl -s $INSTANCE_ENDPOINT_V1 | jq '.stats.user_count')
COUNT_TOTAL_STATUS=$(curl -s $INSTANCE_ENDPOINT_V1 | jq '.stats.status_count')
COUNT_TOTAL_DOMAINS=$(curl -s $INSTANCE_ENDPOINT_V1 | jq '.stats.domain_count')
COUNT_ACTIVE_USERS=$(curl -s $INSTANCE_ENDPOINT_V2 | jq '.usage.users.active_month')
COUNT_POOL_LIMIT=$(curl -s $INSTANCE_ENDPOINT_V2 | jq '.configuration.polls.max_options')
COUNT_CHAR_LIMIT=$(curl -s $INSTANCE_ENDPOINT_V2 | jq '.configuration.statuses.max_characters')
INSTANCE_VERSION=$(curl -s $INSTANCE_ENDPOINT_V2 | jq '.version')

echo "Number of total registered users: $COUNT_TOTAL_USERS"
echo "Number of total statuses: $COUNT_TOTAL_STATUS"
echo "Number of total known domains: $COUNT_TOTAL_DOMAINS"
echo "Number of active users (this month): $COUNT_ACTIVE_USERS"
echo "Number of pool options: $COUNT_POOL_LIMIT"
echo "Number of char limit: $COUNT_CHAR_LIMIT"
echo "Mastodon instance version: $INSTANCE_VERSION"

Execute

$ bash mastodon_api_info.sh

Expected output (from bolha.us)

Number of total registered users: 1464
Number of total statuses: 51191
Number of total known domains: 16237
Number of active users (this month): 618
Number of pool options: 4
Number of char limit: 500
Mastodon instance version: "4.1.2"

:)


18. Upgrade

This process will cover the upgrade for minor versions only.

4.1.x to 4.1.y for example.

Here we'll cover the scenario where mastodon and nginx run on the same server.

18.1 pre-upgrade

  1. run a backup of your postgresql

  2. run a backup of your /opt/mastodon/data/web/system directory

  3. run a backup of your /opt/mastodon/docker directory

  4. run a backup of your /opt/nginx directory

  5. stop mastodon service

  6. stop nginx services

18.2 upgrading

Verify

:)


19. Final notes

I hope you can use this doc to configure your own instance like we did.

I wish to find something like this one year ago when I started my studies in the fediverse tools and Mastodon. :)

If you need assistance, you can reach me on the matrix @gutocarvalho@bolha.chat or mastodon @gutocarvalho@gcn.sh.

:)

This post inspired our post:

Their post inspired the work, and we tried to expand it the best we could.

The first version of bolha.us followed their instructions, and this new post is just to keep the information flowing and updated.

I want to thank the “sleeplessbeastie.eu” team for the excellent work; we are running in prod because of you :)


20. Next posts

1. Enabling Object Storage using Wasabi

2. LibreTranslate installation and integration

3. Individual Sidekiqs Containers For Queues

4. Observability with Prometheus + Grafana

5. Mastodon With TOR

[s] Guto


Did you like our content?

We have a lot to share; visit our site!

Our fediverse services ;)

Chat and video? We have it!

Translation tools

Video Platform Frontends

Text Editors

You can also visit our hacking space!

Follow our founder!

Follow the status of our tools

Do you want to support us? You can!

See you!

[s]

docker install

curl https://get.docker.com | bash

docker-compose install

binary download

curl -s https://api.github.com/repos/docker/compose/releases/latest | grep browser_download_url  | grep docker-compose-linux-x86_64 | cut -d '"' -f 4 | wget -qi -

set permisisons

chmod +x docker-compose-linux-x86_64

moving

mv docker-compose-linux-x86_64 /usr/local/bin/docker-compose

done!

creating directories

mkdir -p /opt/writefreely
mkdir -p /opt/writefreely/{docker,data,code,keys,config}
mkdir -p /opt/writefreely/data/{mariadb,schema}
mkdir -p /opt/writefreely/code/{source,release}

download source code

cd /opt/writefreely/code/source 
wget https://github.com/writefreely/writefreely/archive/refs/tags/v0.13.2.tar.gz 
tar zxvf v0.13.2.tar.gz

download the compiled release

cd /opt/writefreely/code/release
wget https://github.com/writefreely/writefreely/releases/download/v0.13.2/writefreely_0.13.2_linux_amd64.tar.gz
tar zxvf writefreely_0.13.2_linux_amd64.tar.gz

creating the docker-compose config

vim /opt/writefreely/docker/docker-compose.yml

content

version: "3"

services:
  writefreely-web:
    container_name: "writefreely-web"
    image: "writeas/writefreely:latest"
    restart: unless-stopped
    volumes:
      - keys:/go/keys"
      - code:/opt/code"
      - "/opt/writefreely/config/config.ini:/go/config.ini"
     ports:
      - "8080:8080"
    depends_on:
      - "writefreely-db"
    networks:
      - "internal_writefreely"
      - "external_writefreely"

  writefreely-db:
    container_name: "writefreely-db"
    image: "mariadb:latest"
    volumes:
      - ariadb:/var/lib/mysql"
      - schema:/opt/schema"
      - code:/opt/code"
    environment:
      - MYSQL_DATABASE=writefreely
      - MYSQL_ROOT_PASSWORD=your_mariadb_password_here
    restart: unless-stopped
    networks:
      - "internal_writefreely"

networks:
  external_writefreely:
  internal_writefreely:
    internal: true

volumes:
  keys:
    driver_opts:
      type: none
      device: /opt/writefreely/keys
      o: bind    
  code:
    driver_opts:
      type: none
      device: /opt/writefreely/code
      o: bind    
  schema:
    driver_opts:
      type: none
      device: /opt/writefreely/data/schema
      o: bind    
  mariadb:
    driver_opts:
      type: none
      device: /opt/writefreely/data/mariadb
      o: bind

creating the config.ini file

vim /opt/writefreely/config/config.ini

content

[server]
hidden_host          =
port                 = 8080
bind                 = 0.0.0.0
tls_cert_path        =
tls_key_path         =
autocert             = false
templates_parent_dir =
static_parent_dir    =
pages_parent_dir     =
keys_parent_dir      =
hash_seed            =
gopher_port          = 0

[database]
type     = mysql
filename =
username = root
password = your_mariadb_password_here
database = writefreely
host     = writefreely-db
port     = 3306
tls      = false

[app]
site_name             = domain_here
site_description      = A site description here
host                  = https://your_writefreely_domain_here
theme                 = write
editor                =
disable_js            = false
webfonts              = true
landing               =
simple_nav            = false
wf_modesty            = false
chorus                = false
forest                = false
disable_drafts        = false
single_user           = false
open_registration     = false
open_deletion         = false
min_username_len      = 3
max_blogs             = 8
federation            = true
public_stats          = true
monetization          = false
notes_only            = false
private               = false
local_timeline        = true
user_invites          =
default_visibility    = public
update_checks         = true
disable_password_auth = false

[oauth.slack]
client_id          =
client_secret      =
team_id            =
callback_proxy     =
callback_proxy_api =

[oauth.writeas]
client_id          =
client_secret      =
auth_location      =
token_location     =
inspect_location   =
callback_proxy     =
callback_proxy_api =

[oauth.gitlab]
client_id          =
client_secret      =
host               =
display_name       =
callback_proxy     =
callback_proxy_api =

[oauth.gitea]
client_id          =
client_secret      =
host               =
display_name       =
callback_proxy     =
callback_proxy_api =

[oauth.generic]
client_id          =
client_secret      =
host               =
display_name       =
callback_proxy     =
callback_proxy_api =
token_endpoint     =
inspect_endpoint   =
auth_endpoint      =
scope              =
allow_disconnect   = false
map_user_id        =
map_username       =
map_display_name   =
map_email          =

creating the database

cd /opt/writefreely/docker
docker-compose up -d writefreely-db

running the schema

docker-compose exec writefreely-db sh -c 'exec mariadb -u root -pwritefreely-password writefreely < /opt/code/source/writefreely-0.13.2/schema.sql'

creating the keys

cd /opt/writefreely/code/release/writefreely
./writefreely -c /opt/writefreely/config/config.ini --gen-keys
chmod 644 keys/*
cp keys/* /opt/writefreely/keys/

starting writefreely

cd /opt/writefreely/docker
docker-compose up -d writefreely-web

check if it's running

docker-compose logs -f

expected output

writefreely-web  | 2023/06/10 13:09:05 Starting WriteFreely ...
writefreely-web  | 2023/06/10 13:09:05 Loading config.ini configuration...
writefreely-web  | 2023/06/10 13:09:05 Loading templates...
writefreely-web  | 2023/06/10 13:09:05 Loading pages...
writefreely-web  | 2023/06/10 13:09:05 Loading user pages...
writefreely-web  | 2023/06/10 13:09:06 Loading encryption keys...
writefreely-web  | 2023/06/10 13:09:06 Connecting to mysql database...
writefreely-web  | 2023/06/10 13:09:06 Initializing local timeline...
writefreely-web  | 2023/06/10 13:09:06 Adding blog.gcn.sh routes (multi-user)...
writefreely-web  | 2023/06/10 13:09:06 Going to serve...
writefreely-web  | 2023/06/10 13:09:06 Serving on http://0.0.0.0:8080
writefreely-web  | 2023/06/10 13:09:06 ---

initializing the database

cd /opt/writefreely/docker
docker-compose exec writefreely-web sh -c 'exec /go/cmd/writefreely/writefreely -c /go/config.ini --init-db'

expected output

2023/06/10 17:28:55 Loading /go/config.ini configuration...
2023/06/10 17:28:55 Connecting to mysql database...
2023/06/10 17:28:55 Creating table accesstokens...
2023/06/10 17:28:55 Created.
2023/06/10 17:28:55 Creating table appcontent...
2023/06/10 17:28:55 Created.
2023/06/10 17:28:55 Creating table appmigrations...
ERROR: 2023/06/10 17:28:55 app.go:903: Error 1050: Table 'appmigrations' already exists
2023/06/10 17:28:55 Creating table collectionattributes...
2023/06/10 17:28:55 Created.
2023/06/10 17:28:55 Creating table collectionkeys...
2023/06/10 17:28:55 Created.
2023/06/10 17:28:55 Creating table collectionpasswords...
2023/06/10 17:28:55 Created.
2023/06/10 17:28:55 Creating table collectionredirects...
2023/06/10 17:28:55 Created.
2023/06/10 17:28:55 Creating table collections...
2023/06/10 17:28:55 Created.
2023/06/10 17:28:55 Creating table posts...
2023/06/10 17:28:55 Created.
2023/06/10 17:28:55 Creating table remotefollows...
2023/06/10 17:28:55 Created.
2023/06/10 17:28:55 Creating table remoteuserkeys...
2023/06/10 17:28:55 Created.
2023/06/10 17:28:55 Creating table remoteusers...
2023/06/10 17:28:55 Created.
2023/06/10 17:28:55 Creating table userattributes...
2023/06/10 17:28:55 Created.
2023/06/10 17:28:55 Creating table userinvites...
ERROR: 2023/06/10 17:28:55 app.go:903: Error 1050: Table 'userinvites' already exists
2023/06/10 17:28:55 Creating table users...
2023/06/10 17:28:55 Created.
2023/06/10 17:28:55 Creating table usersinvited...
ERROR: 2023/06/10 17:28:55 app.go:903: Error 1050: Table 'usersinvited' already exists
2023/06/10 17:28:55 Initializing appmigrations table...
2023/06/10 17:28:55 Running migrations...
2023/06/10 17:28:55 Migrating to V2: support dynamic instance pages
2023/06/10 17:28:55 Migrating to V3: support users suspension
2023/06/10 17:28:55 Migrating to V4: support oauth
2023/06/10 17:28:55 Migrating to V5: support slack oauth
2023/06/10 17:28:55 Migrating to V6: support ActivityPub mentions
2023/06/10 17:28:55 Migrating to V7: support oauth attach
2023/06/10 17:28:55 Migrating to V8: support oauth via invite
2023/06/10 17:28:55 Migrating to V9: optimize drafts retrieval
2023/06/10 17:28:55 Migrating to V10: support post signatures
2023/06/10 17:28:55 Done.
2023/06/10 17:28:55 Closing database connection...

Yeah, it's done!

nginx config example

server {
    listen put_your_ip_here:80;
    server_name your_writefreely_domain_here;
    location / {
        return 301 https://your_writefreely_domain_here$request_uri;
    }
}

server {
    listen put_your_ip_here:443 ssl http2;
    server_name your_writefreely_domain_here;

    ssl_certificate /etc/letsencrypt/live/your_domain_dir_here/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/your_domain_dir_here/privkey.pem;

    access_log /var/log/nginx/writefreely-access.log;
    error_log /var/log/nginx/gcn-blog-error.log;

    location / {
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_pass http://1.2.3.4:5678;
        proxy_redirect off;
    }
}

creating users

creating an admin user

cd /opt/writefreely/docker
docker-compose exec writefreely-web sh -c 'exec /go/cmd/writefreely/writefreely -c /go/config.ini --create-admin username:password'

ouput expected

2023/06/10 17:47:59 Loading /go/config.ini configuration...
2023/06/10 17:47:59 Connecting to mysql database...
2023/06/10 17:47:59 Creating admin USERNAME...
2023/06/10 17:47:59 Done!
2023/06/10 17:47:59 Closing database connection...

:)

creating a normal user

cd /opt/writefreely/docker
docker-compose exec writefreely-web sh -c 'exec /go/cmd/writefreely/writefreely -c /go/config.ini --create-user username:password'

reseting a user password

cd /opt/writefreely/docker
docker-compose exec writefreely-web sh -c 'exec /go/cmd/writefreely/writefreely -c /go/config.ini --reset-pass username'

Go to your blog!


Did you like our content?

We have a lot to share; visit our site!

Our fediverse services ;)

Chat and video? We have it!

Translation tools

Video Platform Frontends

Text Editors

You can also visit our hacking space!

Follow our founder!

Follow the status of our tools

Do you want to support us? You can!

See you!

[s]